diff --git a/.gitattributes b/.gitattributes index cfa4eea..11aad01 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,7 @@ * text=auto eol=lf *.pp eol=lf linguist-language=EBNF *.pp2 eol=lf linguist-language=EBNF +*.pp3 eol=lf linguist-language=EBNF # GIT .editorconfig export-ignore diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a7df15d..3abc5cb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - php: [ '8.4', '8.5' ] + php: [ '8.1', '8.2', '8.3', '8.4', '8.5' ] os: [ ubuntu-latest, macos-latest, windows-latest ] stability: [ lowest, stable ] steps: diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index d95cfb6..5c5d69d 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -24,7 +24,7 @@ strict: true, )); -return new Config() +return (new Config()) ->setUnsupportedPhpVersionAllowed(true) ->setParallelConfig(ParallelConfigFactory::detect()) ->setRules([ @@ -277,7 +277,7 @@ 'unary_operator_spaces' => true, 'whitespace_after_comma_in_array' => true, 'yoda_style' => false, - 'new_expression_parentheses' => true, + 'new_expression_parentheses' => ['use_parentheses' => true], ]) ->setCacheFile(__DIR__ . '/vendor/.cache.php-cs-fixer') ->setFinder($files); diff --git a/bench/Parser/StringDecoding/PHPStanStringUnescaperBench.php b/bench/Parser/StringDecoding/PHPStanStringUnescaperBench.php new file mode 100644 index 0000000..1deb87f --- /dev/null +++ b/bench/Parser/StringDecoding/PHPStanStringUnescaperBench.php @@ -0,0 +1,23 @@ + + */ + private const REPEATS = 16; + + /** + * @return iterable + */ + public static function stringsDataProvider(): iterable + { + yield 'plain' => ['string' => '"example"', 'double' => true]; + yield 'plain long' => ['string' => '"' . \str_repeat(self::LOREM, 8) . '"', 'double' => true]; + yield 'quote' => ['string' => '"a\"b"', 'double' => true]; + yield 'few specials' => ['string' => '"line\nand\ttab"', 'double' => true]; + yield 'many specials' => ['string' => '"' . \str_repeat('a\nb\tc\r', self::REPEATS) . '"', 'double' => true]; + yield 'many backslashes' => ['string' => '"' . \str_repeat('a\\\\b', self::REPEATS) . '"', 'double' => true]; + yield 'one hexadecimal' => ['string' => '"a\x41b"', 'double' => true]; + yield 'numeric mix' => ['string' => '"\x41\101\u{1F600}\x42"', 'double' => true]; + yield 'many numeric' => ['string' => '"' . \str_repeat('\x41\101\u{48}', self::REPEATS) . '"', 'double' => true]; + yield 'single quoted plain' => ['string' => "'" . \str_repeat(self::LOREM, 8) . "'", 'double' => false]; + yield 'single quoted escaped' => ['string' => "'" . \str_repeat('a\\\\b\\\'c', self::REPEATS) . "'", 'double' => false]; + } + + /** + * @param array{string: non-empty-string, double: bool} $params + */ + abstract public function benchDecodeString(array $params): void; +} diff --git a/bench/Parser/StringDecoding/TypeLangStringDecoderBench.php b/bench/Parser/StringDecoding/TypeLangStringDecoderBench.php new file mode 100644 index 0000000..05592bc --- /dev/null +++ b/bench/Parser/StringDecoding/TypeLangStringDecoderBench.php @@ -0,0 +1,23 @@ + + */ + protected const CORPUS_PACKAGES = [ + 'doctrine/collections' => '*', + 'doctrine/lexer' => '*', + 'guzzlehttp/guzzle' => '*', + 'guzzlehttp/promises' => '*', + 'guzzlehttp/psr7' => '*', + 'illuminate/support' => '*', + 'monolog/monolog' => '*', + 'nikic/php-parser' => '*', + 'phpdocumentor/reflection-docblock' => '*', + 'phpstan/extension-installer' => '*', + 'phpstan/php-8-stubs' => '*', + 'phpstan/phpdoc-parser' => '*', + 'phpstan/phpstan' => '*', + 'phpstan/phpstan-beberlei-assert' => '*', + 'phpstan/phpstan-deprecation-rules' => '*', + 'phpstan/phpstan-dibi' => '*', + 'phpstan/phpstan-doctrine' => '*', + 'phpstan/phpstan-mockery' => '*', + 'phpstan/phpstan-nette' => '*', + 'phpstan/phpstan-php-parser' => '*', + 'phpstan/phpstan-phpunit' => '*', + 'phpstan/phpstan-strict-rules' => '*', + 'phpstan/phpstan-symfony' => '*', + 'phpstan/phpstan-webmozart-assert' => '*', + 'psalm/attributes' => '*', + 'psalm/plugin-mockery' => '*', + 'psalm/plugin-phpunit' => '*', + 'psalm/plugin-symfony' => '*', + 'phpunit/php-code-coverage' => '*', + 'phpunit/php-file-iterator' => '*', + 'phpunit/php-text-template' => '*', + 'phpunit/php-timer' => '*', + 'phpunit/phpunit' => '*', + 'psr/container' => '*', + 'psr/http-message' => '*', + 'psr/log' => '*', + 'ralouphie/getallheaders' => '*', + 'sebastian/comparator' => '*', + 'sebastian/diff' => '*', + 'sebastian/environment' => '*', + 'sebastian/exporter' => '*', + 'sebastian/recursion-context' => '*', + 'symfony/console' => '*', + 'symfony/event-dispatcher' => '*', + 'symfony/finder' => '*', + 'symfony/http-foundation' => '*', + 'symfony/polyfill-ctype' => '*', + 'symfony/polyfill-mbstring' => '*', + 'symfony/process' => '*', + 'symfony/service-contracts' => '*', + 'symfony/var-dumper' => '*', + 'twig/twig' => '*', + 'vimeo/psalm' => '*', + 'webmozart/assert' => '*', + ]; + + /** + * The PHP version the corpus is resolved for. + * + * The corpus is read and never run, so the version it is installed for is + * not the one the benchmark runs on: it is pinned so that the corpus is + * the same everywhere, and so that a package supporting no version as new + * as the one at hand is installed all the same. + * + * @var non-empty-string + */ + protected const CORPUS_PHP_VERSION = '8.3.0'; + + /** + * Each set contains every DocBlock of a single package of the corpus. + * + * The corpus is installed by the provider itself, because it is executed + * in a separate process before any benchmark is launched. + * + * @return iterable}> + * @throws \JsonException + */ + public static function docBlocksDataProvider(): iterable + { + $metadata = self::getCorpusMetadata(); + + foreach (\array_keys(self::CORPUS_PACKAGES) as $package) { + $directory = \realpath($metadata['versions'][$package]['install_path'] ?? ''); + + if ($directory === false) { + throw new \RuntimeException(\sprintf( + 'The "%s" package of the benchmark corpus is not installed', + $package, + )); + } + + $docblocks = self::extractDocBlocks($directory); + + if ($docblocks === []) { + continue; + } + + yield $package => ['docblocks' => $docblocks]; + } + } + + /** + * Installs the corpus (if required) and returns the Composer metadata + * of the installed packages. + * + * @return array{versions: array} + * @throws \JsonException + */ + private static function getCorpusMetadata(): array + { + $installed = self::CORPUS_DIRECTORY . '/vendor/composer/installed.php'; + + // The manifest is generated from the {@see CORPUS_PACKAGES} list, so + // that an edit of the list is enough to update the corpus. + $outdated = self::updateCorpusManifest(); + + if ($outdated || !\is_file(self::CORPUS_DIRECTORY . '/composer.lock')) { + self::composer('update'); + } elseif (!\is_file($installed)) { + self::composer('install'); + } + + /** @var array{versions: array} */ + return require $installed; + } + + /** + * Writes the "composer.json" of the corpus and returns {@see true} in + * case of the manifest has been changed (and therefore the installed + * packages are outdated). + */ + private static function updateCorpusManifest(): bool + { + $pathname = self::CORPUS_DIRECTORY . '/composer.json'; + + $expected = \json_encode([ + 'name' => 'type-lang/bench-corpus', + 'description' => 'A disposable sandbox of the third-party packages that are ' + . 'used as a real-world DocBlock corpus. The directory is generated by ' + . 'the benchmarks and can be safely removed.', + 'type' => 'project', + 'license' => 'MIT', + 'require' => self::CORPUS_PACKAGES, + 'config' => [ + 'preferred-install' => ['*' => 'dist'], + 'allow-plugins' => false, + 'platform' => ['php' => self::CORPUS_PHP_VERSION], + ], + 'minimum-stability' => 'stable', + 'prefer-stable' => true, + ], \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_THROW_ON_ERROR); + + if (@\file_get_contents($pathname) === $expected) { + return false; + } + + if (!\is_dir(self::CORPUS_DIRECTORY) && !@\mkdir(self::CORPUS_DIRECTORY, recursive: true)) { + throw new \RuntimeException(\sprintf( + 'Could not create the benchmark corpus directory "%s"', + self::CORPUS_DIRECTORY, + )); + } + + if (\file_put_contents($pathname, $expected) === false) { + throw new \RuntimeException(\sprintf( + 'Could not write the benchmark corpus manifest "%s"', + $pathname, + )); + } + + return true; + } + + /** + * @param non-empty-string $command + */ + private static function composer(string $command): void + { + $process = new Process( + command: [ + ...self::getComposerBinary(), + $command, + '--working-dir=' . self::CORPUS_DIRECTORY, + '--no-interaction', + '--no-progress', + '--no-audit', + '--no-plugins', + '--no-scripts', + '--prefer-dist', + ], + // The installation of the corpus may take a few minutes on the + // first run, so it should not be interrupted by a timeout. + timeout: null, + ); + + if ($process->run() !== 0) { + throw new \RuntimeException(\sprintf( + "Could not install the benchmark corpus:\n%s", + \trim($process->getErrorOutput() . "\n" . $process->getOutput()), + )); + } + } + + /** + * @return non-empty-list + */ + private static function getComposerBinary(): array + { + $finder = new ExecutableFinder(); + + foreach (['composer', 'composer.phar'] as $name) { + $pathname = $finder->find($name); + + if ($pathname === null || $pathname === '') { + continue; + } + + // A phar is not executable on all platforms, so it is passed + // to the PHP binary that runs the benchmarks. + return \str_ends_with($pathname, '.phar') + ? [\PHP_BINARY, $pathname] + : [$pathname]; + } + + throw new \RuntimeException(\sprintf( + 'The "composer" executable is required to install the benchmark corpus, ' + . 'but it could not be found. Please install the corpus manually ' + . 'using the "composer install --working-dir=%s" command', + self::CORPUS_DIRECTORY, + )); + } + + /** + * Extracts every DocBlock of every PHP file of the given directory. + * + * @param non-empty-string $directory + * @return list + */ + private static function extractDocBlocks(string $directory): array + { + $result = []; + + foreach (self::getSourceFiles($directory) as $pathname) { + $source = @\file_get_contents($pathname); + + if ($source === false || $source === '') { + continue; + } + + foreach (\PhpToken::tokenize($source) as $token) { + if ($token->is(\T_DOC_COMMENT) && $token->text !== '') { + $result[] = $token->text; + } + } + } + + return $result; + } + + /** + * @param non-empty-string $directory + * @return iterable + */ + private static function getSourceFiles(string $directory): iterable + { + $files = new \RecursiveIteratorIterator( + iterator: new \RecursiveDirectoryIterator( + directory: $directory, + flags: \FilesystemIterator::SKIP_DOTS + | \FilesystemIterator::CURRENT_AS_FILEINFO, + ), + ); + + /** @var \SplFileInfo $file */ + foreach ($files as $file) { + $pathname = $file->getPathname(); + + if ($file->isFile() && $file->getExtension() === 'php' && $pathname !== '') { + yield $pathname; + } + } + } + + /** + * @param array{docblocks: list} $params + */ + abstract public function benchParseDocBlock(array $params): void; +} diff --git a/libs/phpdoc/tests/Bench/PHPStanParserBench.php b/bench/PhpDoc/Tools/PHPStanParserBench.php similarity index 55% rename from libs/phpdoc/tests/Bench/PHPStanParserBench.php rename to bench/PhpDoc/Tools/PHPStanParserBench.php index 2e265ca..d159a12 100644 --- a/libs/phpdoc/tests/Bench/PHPStanParserBench.php +++ b/bench/PhpDoc/Tools/PHPStanParserBench.php @@ -2,10 +2,12 @@ declare(strict_types=1); -namespace TypeLang\PhpDoc\Tests\Bench; +namespace TypeLang\Bench\PhpDoc\Tools; use PhpBench\Attributes\BeforeMethods; +use PhpBench\Attributes\Groups; use PhpBench\Attributes\Iterations; +use PhpBench\Attributes\ParamProviders; use PhpBench\Attributes\RetryThreshold; use PhpBench\Attributes\Revs; use PhpBench\Attributes\Warmup; @@ -16,8 +18,9 @@ use PHPStan\PhpDocParser\Parser\TypeParser; use PHPStan\PhpDocParser\ParserConfig; -#[Revs(20), Warmup(5), Iterations(15), BeforeMethods('prepare'), RetryThreshold(2)] -final readonly class PHPStanParserBench extends DocBlockParserBench +#[Groups(['phpstan']), Revs(3), Warmup(10), Iterations(5)] +#[BeforeMethods('prepare'), RetryThreshold(5)] +final class PHPStanParserBench extends DocBlockParserBench { private Lexer $lexer; private PhpDocParser $parser; @@ -37,10 +40,17 @@ public function prepare(): void $this->parser = new PhpDocParser($config, $typeParser, $constExprParser); } - public function benchParseDocBlock(): void + #[ParamProviders('docBlocksDataProvider')] + public function benchParseDocBlock(array $params): void { - $iterator = new TokenIterator($this->lexer->tokenize(self::DOC_BLOCK_SAMPLE)); - - $this->parser->parse($iterator); + foreach ($params['docblocks'] as $docblock) { + try { + $this->parser->parse(new TokenIterator($this->lexer->tokenize($docblock))); + } catch (\Throwable) { + // A real-world corpus contains DocBlocks that some of the tools + // are not able to parse. They are skipped so that every tool + // is measured on the same corpus. + } + } } } diff --git a/bench/PhpDoc/Tools/PhpDocumentorParserBench.php b/bench/PhpDoc/Tools/PhpDocumentorParserBench.php new file mode 100644 index 0000000..d8c565f --- /dev/null +++ b/bench/PhpDoc/Tools/PhpDocumentorParserBench.php @@ -0,0 +1,41 @@ +parser = DocBlockFactory::createInstance(); + } + + #[ParamProviders('docBlocksDataProvider')] + public function benchParseDocBlock(array $params): void + { + foreach ($params['docblocks'] as $docblock) { + try { + $this->parser->create($docblock); + } catch (\Throwable) { + // A real-world corpus contains DocBlocks that some of the tools + // are not able to parse. They are skipped so that every tool + // is measured on the same corpus. + } + } + } +} diff --git a/bench/PhpDoc/Tools/TypeLangParserBench.php b/bench/PhpDoc/Tools/TypeLangParserBench.php new file mode 100644 index 0000000..9bd2577 --- /dev/null +++ b/bench/PhpDoc/Tools/TypeLangParserBench.php @@ -0,0 +1,41 @@ +parser = DocBlockParser::createDefault(); + } + + #[ParamProviders('docBlocksDataProvider')] + public function benchParseDocBlock(array $params): void + { + foreach ($params['docblocks'] as $docblock) { + try { + $this->parser->parse($docblock); + } catch (\Throwable) { + // A real-world corpus contains DocBlocks that some of the tools + // are not able to parse. They are skipped so that every tool + // is measured on the same corpus. + } + } + } +} diff --git a/bench/var/.gitignore b/bench/var/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/bench/var/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/composer.json b/composer.json index cc49d4e..202428f 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ {"type": "path", "url": "libs/*"} ], "require": { - "php": "^8.4", + "php": "^8.1", "type-lang/parser": "^2.0", "type-lang/phpdoc": "^2.0", "type-lang/printer": "^2.0", @@ -15,25 +15,26 @@ "type-lang/types": "^2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.95.13", + "friendsofphp/php-cs-fixer": "^3.95", "jetbrains/phpstorm-attributes": "^1.2", - "phpbench/phpbench": "^1.7.0", - "phpdocumentor/reflection-docblock": "^6.0.3", - "phplrt/compiler": "^3.7.5", - "phpstan/phpdoc-parser": "^2.3.3", - "phpstan/phpstan": "^2.2.5", - "phpstan/phpstan-deprecation-rules": "^2.0.4", - "phpstan/phpstan-strict-rules": "^2.0.11", - "phpunit/phpunit": "^13.2.4", - "symfony/var-dumper": "^7.4|^8.0" + "phpbench/phpbench": "^1.2", + "phpdocumentor/reflection-docblock": "^6.0", + "phplrt/compiler": "^4.1", + "phpstan/phpdoc-parser": "^2.3", + "phpstan/phpstan": "^2.2", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^10.5|^11.0|^12.0|^13.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "autoload-dev": { "psr-4": { - "TypeLang\\Parser\\Tests\\": "libs/parser/tests", - "TypeLang\\Printer\\Tests\\": "libs/printer/tests", - "TypeLang\\PhpDoc\\Tests\\": "libs/phpdoc/tests", - "TypeLang\\Reader\\Tests\\": "libs/reader/tests", - "TypeLang\\Type\\Tests\\": "libs/types/tests" + "TypeLang\\Bench\\": "bench", + "TypeLang\\Parser\\Tests\\": "libs/parser/tests", + "TypeLang\\Printer\\Tests\\": "libs/printer/tests", + "TypeLang\\PhpDoc\\Tests\\": "libs/phpdoc/tests", + "TypeLang\\Reader\\Tests\\": "libs/reader/tests", + "TypeLang\\Type\\Tests\\": "libs/types/tests" } }, "extra": { @@ -56,7 +57,8 @@ "phpcs:check": ["phpcs", "cs", "psr", "per"] }, "scripts": { - "bench": "phpbench run libs/phpdoc/tests --report=default", + "bench": "phpbench run ./bench --report=compare", + "bench:census": "php bench/PhpDoc/bin/census", "test:unit": "phpunit --testdox", "linter:check": "phpstan analyse --configuration phpstan.neon --memory-limit 256M", "linter:baseline": "@linter:check --generate-baseline", diff --git a/docs/Specification/Appendix B -- Grammar Summary.md b/docs/Specification/Appendix B -- Grammar Summary.md index 61b2c81..db3afdb 100644 --- a/docs/Specification/Appendix B -- Grammar Summary.md +++ b/docs/Specification/Appendix B -- Grammar Summary.md @@ -16,7 +16,7 @@ Letter :: one of - `N` `O` `P` `Q` `R` `S` `T` `U` `V` `W` `X` `Y` `Z` - `a` `b` `c` `d` `e` `f` `g` `h` `i` `j` `k` `l` `m` - `n` `o` `p` `q` `r` `s` `t` `u` `v` `w` `x` `y` `z` -- "Any byte from U+0080 to U+00FF" +- "Any byte from 0x80 to 0xFF" Digit :: one of `0` `1` `2` `3` `4` `5` `6` `7` `8` `9` @@ -27,7 +27,14 @@ Ignored :: - Whitespace - Comment -Whitespace :: "Any Unicode whitespace character" +Whitespace :: one of + +- "Horizontal Tab (U+0009)" +- "Line Feed (U+000A)" +- "Vertical Tab (U+000B)" +- "Form Feed (U+000C)" +- "Carriage Return (U+000D)" +- "Space (U+0020)" LineTerminator :: one of "New Line (U+000A)" "Carriage Return (U+000D)" @@ -63,8 +70,8 @@ Punctuator :: one of - `?` `|` `&` `*` `,` `:` `=` - `(` `)` `[` `]` `{` `}` -- `<` `>` `<=` `>=` -- `::` `\` `...` `#[` +- `<` `>` +- `::` `\` `...` NameToken :: NameStart NameContinue\* [lookahead != NameContinue] @@ -80,11 +87,13 @@ NameContinue :: - `_` - `-` -ReservedWord :: one of `true` `false` `null` `is` +ReservedWord :: one of `true` `false` `null` `is` `not` + +Variable :: `$` NameStart VariableContinue\* -Variable :: `$` NameStart NameContinue\* +VariableContinue :: NameContinue but not `-` -ThisVariable :: `$this` [lookahead != NameContinue] +ThisVariable :: `$this` [lookahead != VariableContinue] **Literal Tokens** @@ -94,8 +103,12 @@ NullLiteral :: `null` [lookahead != NameContinue] NegativeSign :: `-` +Sign :: one of `-` `+` + DigitSeparator :: `_` +Digits :: Digit (DigitSeparator? Digit)\* + IntLiteral :: - BinaryIntLiteral @@ -103,24 +116,38 @@ IntLiteral :: - HexIntLiteral - DecimalIntLiteral -DecimalIntLiteral :: NegativeSign? Digit (Digit | DigitSeparator)\* +DecimalIntLiteral :: -BinaryIntLiteral :: NegativeSign? `0` BinaryIndicator BinaryDigit (BinaryDigit | DigitSeparator)\* +- Sign? NonZeroDigit (DigitSeparator? Digit)\* +- Sign? `0` + +NonZeroDigit :: one of `1` `2` `3` `4` `5` `6` `7` `8` `9` + +BinaryIntLiteral :: Sign? `0` BinaryIndicator BinaryDigits BinaryIndicator :: one of `b` `B` +BinaryDigits :: BinaryDigit (DigitSeparator? BinaryDigit)\* + BinaryDigit :: one of `0` `1` -OctalIntLiteral :: NegativeSign? `0` OctalIndicator OctalDigit (OctalDigit | DigitSeparator)\* +OctalIntLiteral :: + +- Sign? `0` OctalIndicator OctalDigits +- Sign? `0` (DigitSeparator? OctalDigit)+ OctalIndicator :: one of `o` `O` +OctalDigits :: OctalDigit (DigitSeparator? OctalDigit)\* + OctalDigit :: one of `0` `1` `2` `3` `4` `5` `6` `7` -HexIntLiteral :: NegativeSign? `0` HexIndicator HexDigit (HexDigit | DigitSeparator)\* +HexIntLiteral :: Sign? `0` HexIndicator HexDigits HexIndicator :: one of `x` `X` +HexDigits :: HexDigit (DigitSeparator? HexDigit)\* + HexDigit :: one of - `0` `1` `2` `3` `4` `5` `6` `7` `8` `9` @@ -133,13 +160,13 @@ FloatLiteral :: - TrailingFloatLiteral - ExponentFloatLiteral -LeadingFloatLiteral :: NegativeSign? Digit+ `.` Digit\* ExponentPart? +LeadingFloatLiteral :: Sign? Digits `.` Digits? ExponentPart? -TrailingFloatLiteral :: NegativeSign? Digit\* `.` Digit+ ExponentPart? +TrailingFloatLiteral :: Sign? `.` Digits ExponentPart? -ExponentFloatLiteral :: NegativeSign? Digit+ ExponentPart +ExponentFloatLiteral :: Sign? Digits ExponentPart -ExponentPart :: ExponentIndicator NegativeSign? Digit+ +ExponentPart :: ExponentIndicator Sign? Digits ExponentIndicator :: one of `e` `E` @@ -176,6 +203,8 @@ UnicodeEscape :: `u` `{` HexDigit+ `}` **Types** +Document : Type + Type : Expression Expression : ConditionalType @@ -192,7 +221,7 @@ ConditionalOperand : - Type - Variable -ConditionalOperator : one of `is` `is not` `>=` `<=` `<` `>` +ConditionalOperator : one of `is` `is not` **Logical Types** @@ -251,11 +280,13 @@ NamedType : Name (TemplateArguments | ShapeFields)? TemplateArguments : `<` TemplateArgument (`,` TemplateArgument)\* `,`? `>` -TemplateArgument : AttributeGroups? (TemplateArgumentHint | TemplateArgumentType) +TemplateArgument : TemplateArgumentHint | TemplateArgumentValue + +TemplateArgumentHint : NameToken TemplateArgumentValue -TemplateArgumentHint : Identifier Type +TemplateArgumentValue : Wildcard | Type -TemplateArgumentType : Type +Wildcard : `*` **Literal and Constant Types** @@ -273,9 +304,23 @@ ClassConstant : Name `::` Identifier ConstantMask : -- Name `*` -- Name `::` Identifier `*` -- Name `::` `*` +- GlobalConstantMask +- ClassConstantMask + +GlobalConstantMask : + +- Name MaskTail +- Name `\` MaskTail +- LeadingMask + +ClassConstantMask : + +- Name `::` Identifier MaskTail? +- Name `::` MaskTail + +MaskTail : Wildcard (Identifier Wildcard)\* Identifier? + +LeadingMask : Wildcard Identifier MaskTail? **Shape Types** @@ -288,7 +333,7 @@ ShapeBody : ShapeFieldList : ShapeField (`,` ShapeField)\* -ShapeField : AttributeGroups? (ExplicitField | ImplicitField) +ShapeField : ExplicitField | ImplicitField ExplicitField : ShapeKey `?`? `:` ShapeValue @@ -308,34 +353,27 @@ UnsealedShape : `...` TemplateArguments? **Callable Types** -CallableType : Name `(` CallableParameters? `)` CallableReturnType? - -CallableParameters : CallableParameter (`,` CallableParameter)\* `,`? - -CallableReturnType : `:` Type +CallableType : Name TemplateParameters? `(` CallableParameters? `)` CallableReturnType? -CallableParameter : AttributeGroups? CallableParameterBody `=`? +TemplateParameters : `<` TemplateParameter (`,` TemplateParameter)\* `,`? `>` -CallableParameterBody : +TemplateParameter : Identifier TemplateBound\* TemplateDefault? -- `...`? Type ParameterModifiers? Variable? -- ParameterModifiers? Variable +TemplateBound : -ParameterModifiers : +- UpperBound +- LowerBound -- `&` `...`? -- `...` `&`? +UpperBound : UpperBoundOperator Type -**Attributes** +UpperBoundOperator : one of `of` `as` -AttributeGroups : AttributeGroup+ +LowerBound : `super` Type -AttributeGroup : `#[` AttributeList `,`? `]` +TemplateDefault : `=` Type -AttributeList : Attribute (`,` Attribute)\* - -Attribute : Name AttributeArguments? +CallableParameters : CallableParameter (`,` CallableParameter)\* `,`? -AttributeArguments : `(` AttributeArgument (`,` AttributeArgument)\* `,`? `)` +CallableReturnType : `:` Type -AttributeArgument : Type +CallableParameter : Type `&`? `...`? Variable? `=`? diff --git a/docs/Specification/Section 1 -- Overview.md b/docs/Specification/Section 1 -- Overview.md index b612849..28aa172 100644 --- a/docs/Specification/Section 1 -- Overview.md +++ b/docs/Specification/Section 1 -- Overview.md @@ -103,8 +103,6 @@ others: - Escape, hexadecimal and Unicode sequences inside double-quoted string literals. - Global constant masks such as `JSON_*`. -- Attributes (`#[...]`) on template arguments, shape fields and callable - parameters. - A uniform conditional (ternary) type syntax that also permits comparison operators and Yoda-style operands. diff --git a/docs/Specification/Section 2 -- Lexical Grammar.md b/docs/Specification/Section 2 -- Lexical Grammar.md index e68d8ac..f9d3506 100644 --- a/docs/Specification/Section 2 -- Lexical Grammar.md +++ b/docs/Specification/Section 2 -- Lexical Grammar.md @@ -56,7 +56,7 @@ Letter :: one of - `N` `O` `P` `Q` `R` `S` `T` `U` `V` `W` `X` `Y` `Z` - `a` `b` `c` `d` `e` `f` `g` `h` `i` `j` `k` `l` `m` - `n` `o` `p` `q` `r` `s` `t` `u` `v` `w` `x` `y` `z` -- "Any byte from U+0080 to U+00FF" +- "Any byte from 0x80 to 0xFF" Digit :: one of `0` `1` `2` `3` `4` `5` `6` `7` `8` `9` @@ -72,23 +72,36 @@ otherwise insignificant. Any amount of {Ignored} MAY appear before and after every lexical {Token}. Note: Although {Ignored} between two tokens is never itself significant, the -_presence_ of separating whitespace is what distinguishes a {NameToken} that -is immediately followed by another token from a {NameToken} that introduces a -template argument hint (see +_presence_ of {Whitespace} standing directly behind a {NameToken} is what +distinguishes a {NameToken} that is immediately followed by another token +from a {NameToken} that introduces a template argument hint (see [Template Argument Hints](#sec-Template-Argument-Hints)). This is the only -place in the grammar where the presence of separating whitespace is -observable to the syntactic grammar. +place in the grammar where {Ignored} is observable to the syntactic grammar, +and the one place a {Comment} does not stand for the whitespace it is written +among: a comment MAY follow that whitespace, but MUST NOT stand in its stead. ### White Space -Whitespace :: "Any Unicode whitespace character" +Whitespace :: one of -Whitespace separates tokens and improves the legibility of the source text. It -includes, at minimum, the space (U+0020), horizontal tab (U+0009), line feed -(U+000A) and carriage return (U+000D) characters. TypeLang does not -distinguish between horizontal whitespace and line terminators, except within -the multi-word `is not` operator (see -[Conditional Types](#sec-Conditional-Types)). +- "Horizontal Tab (U+0009)" +- "Line Feed (U+000A)" +- "Vertical Tab (U+000B)" +- "Form Feed (U+000C)" +- "Carriage Return (U+000D)" +- "Space (U+0020)" + +Whitespace separates tokens and improves the legibility of the source text. +TypeLang does not distinguish between horizontal whitespace and line +terminators, and a {Comment} stands wherever whitespace stands, save for the +one place the presence of the whitespace itself is read (see +[Ignored Tokens](#sec-Ignored-Tokens)). + +Note: The six characters above are the whole of it. A source text is read as +a sequence of bytes (see [Source Text](#sec-Source-Text)), so a character that +parts words in a script of its own — a no-break space (U+00A0), say — is no +whitespace here: every byte it is written of falls in the range a {Letter} +covers, and it is read as a part of the name it stands in. ### Comments @@ -116,11 +129,6 @@ comment_ begins with `/*` and continues up to and including the next `*/`. Comments are {Ignored} and have no bearing on the meaning of a document. -Note: The `#` line comment marker and the `#[` attribute marker share a -leading character. Because lexical analysis prefers the longest match, the -sequence `#[` is always scanned as the start of an attribute (see -[Attributes](#sec-Attributes)) rather than as the start of a comment. - ## Lexical Tokens Token :: @@ -145,13 +153,12 @@ Punctuator :: one of - `?` `|` `&` `*` `,` `:` `=` - `(` `)` `[` `]` `{` `}` -- `<` `>` `<=` `>=` -- `::` `\` `...` `#[` +- `<` `>` +- `::` `\` `...` TypeLang documents use punctuation to describe structure. Several punctuators -share a leading character (for example, `<` and `<=`; `:` and `::`; `.` in -`...`; `#` in `#[`); in every such case, the longest matching punctuator is -taken. +share a leading character (for example, `:` and `::`; `.` in `...`); in every +such case, the longest matching punctuator is taken. ### Names @@ -179,6 +186,14 @@ the [PHP identifier grammar](https://www.php.net/manual/en/language.variables.ba is that the dash character is additionally permitted in any non-leading position. +A source text is read as a sequence of bytes rather than of codepoints, and +every byte of a character outside of ASCII falls in the 0x80 to 0xFF range a +{Letter} covers. A name MAY therefore be written in any script. + +```typescript +Проект\Тип +``` + A {NameToken} is always the longest possible valid sequence; it MUST NOT be followed by a {NameContinue} character. @@ -213,41 +228,57 @@ ParseException: Syntax error, unexpected "type" ### Reserved Words -ReservedWord :: one of `true` `false` `null` `is` +ReservedWord :: one of `true` `false` `null` `is` `not` The words `true`, `false` and `null` are _literal_ keywords (see -[Literal Tokens](#sec-Literal-Tokens)), and `is` is the conditional operator -keyword (see [Conditional Types](#sec-Conditional-Types)). All four are -matched case-insensitively, and only when not immediately followed by a -{NameContinue} character. +[Literal Tokens](#sec-Literal-Tokens)), and `is` and `not` are the +conditional operator keywords (see +[Conditional Types](#sec-Conditional-Types)). Each is matched only when not +immediately followed by a {NameContinue} character. The three literal +keywords are matched case-insensitively, while `is` and `not` are matched +the one way they are spelled. + +Wherever the source contains one of these words, it is scanned as the +corresponding literal or operator token and MUST NOT be scanned as a +{NameToken}. It remains an {Identifier} all the same (see +[Names and Namespaces](#sec-Names-and-Namespaces)), so a reserved word is +shadowed rather than forbidden: it stands as a name wherever the token it is +scanned as denotes no type of its own. + +A literal keyword standing alone, in a position where a type is expected, is +read as the {LiteralType} it denotes and not as the {NamedType} it would +otherwise be (see [Primary Types](#sec-Primary-Types)): -When the source contains one of these words standing alone, in a position -where a type is expected, it is scanned as the corresponding literal or -operator token and MUST NOT be scanned as a {NameToken}. Consequently, a bare -reserved word cannot be used as a type name. +```typescript +TrUe +``` -**Counter-example.** A bare reserved word is scanned as a literal, not a type -name. +Behind a namespace separator or another segment, where no literal is +expected, the same word is a name segment. This makes `\true` a reference to +a type literally named `true`, distinct from the `true` literal: -```typescript counter-example -TrUe +```typescript +\true ``` -A reserved word MAY, however, appear as an {Identifier} _inside_ a qualified -{Name} — that is, when it is preceded by a namespace separator or another -identifier (see [Names and Namespaces](#sec-Names-and-Namespaces)). For -example, `\true` references a type literally named `true`, whereas the bare -`true` is the boolean literal. +The conditional operator keywords denote no type of their own, so nothing +shadows them, and a bare `is` or `not` is an ordinary named type: + +```typescript +not +``` ### Variable -Variable :: `$` NameStart NameContinue\* +Variable :: `$` NameStart VariableContinue\* -ThisVariable :: `$this` [lookahead != NameContinue] +VariableContinue :: NameContinue but not `-` + +ThisVariable :: `$this` [lookahead != VariableContinue] A {Variable} token begins with a dollar sign (`$`) followed by a sequence -matching the body of a {NameToken}. Variables are used to name callable -parameters (see [Callable Types](#sec-Callable-Types)) and as operands in +matching the body of a {NameToken}, save for the dash a variable cannot +carry. Variables are used to name callable parameters (see [Callable Types](#sec-Callable-Types)) and as operands in conditional types (see [Conditional Types](#sec-Conditional-Types)). The special variable `$this` is recognised as a distinct token and @@ -289,17 +320,30 @@ IntLiteral :: NegativeSign :: `-` +Sign :: one of `-` `+` + DigitSeparator :: `_` An integer literal denotes a value of the PHP `int` type. Binary, octal, decimal and hexadecimal radixes are supported, each optionally prefixed with a -{NegativeSign}. Within the digits of any integer literal, underscores -({DigitSeparator}) MAY appear freely as visual separators; they carry no -meaning and do not affect the denoted value. +{Sign}. A {NegativeSign} makes the denoted value negative; a leading `+` denotes +the same value the absence of a {Sign} does. Underscores ({DigitSeparator}) MAY +appear as visual separators; they carry no meaning and do not affect the denoted +value. A {DigitSeparator} MUST stand between two digits, and therefore may +neither lead a run of digits, nor trail one, nor stand beside another +{DigitSeparator}. + +Digits :: Digit (DigitSeparator? Digit)\* + +**Decimal.** A decimal literal other than a lone `0` MUST NOT begin with a +zero, which is the mark of an {OctalIntLiteral}. + +DecimalIntLiteral :: -**Decimal.** +- Sign? NonZeroDigit (DigitSeparator? Digit)\* +- Sign? `0` -DecimalIntLiteral :: NegativeSign? Digit (Digit | DigitSeparator)\* +NonZeroDigit :: one of `1` `2` `3` `4` `5` `6` `7` `8` `9` ```typescript 1_000_000 @@ -307,35 +351,61 @@ DecimalIntLiteral :: NegativeSign? Digit (Digit | DigitSeparator)\* **Binary.** Prefixed with `0b` or `0B`; digits are `0` and `1` only. -BinaryIntLiteral :: NegativeSign? `0` BinaryIndicator BinaryDigit (BinaryDigit | DigitSeparator)\* +BinaryIntLiteral :: Sign? `0` BinaryIndicator BinaryDigits BinaryIndicator :: one of `b` `B` +BinaryDigits :: BinaryDigit (DigitSeparator? BinaryDigit)\* + BinaryDigit :: one of `0` `1` ```typescript 0b1010_1101 ``` -**Octal.** Prefixed with `0o` or `0O`; digits are `0` through `7` only. +**Octal.** Prefixed with `0o` or `0O` or, as octal has been written since long +before that prefix, with a leading `0` alone; digits are `0` through `7` only. +A `0123` denotes the same value a `0o123` does. + +OctalIntLiteral :: -OctalIntLiteral :: NegativeSign? `0` OctalIndicator OctalDigit (OctalDigit | DigitSeparator)\* +- Sign? `0` OctalIndicator OctalDigits +- Sign? `0` (DigitSeparator? OctalDigit)+ OctalIndicator :: one of `o` `O` +OctalDigits :: OctalDigit (DigitSeparator? OctalDigit)\* + OctalDigit :: one of `0` `1` `2` `3` `4` `5` `6` `7` ```typescript 0o42 ``` +```typescript +04_23 +``` + +**Counter-example.** A leading zero makes a literal an octal, so a digit +outside the octal radix may not follow one. + +```typescript counter-example +08 +``` + +``` +ParseException: Syntax error, unexpected "8" +``` + **Hexadecimal.** Prefixed with `0x` or `0X`; digits are `0` through `9` and `a` through `f`, in either case. -HexIntLiteral :: NegativeSign? `0` HexIndicator HexDigit (HexDigit | DigitSeparator)\* +HexIntLiteral :: Sign? `0` HexIndicator HexDigits HexIndicator :: one of `x` `X` +HexDigits :: HexDigit (DigitSeparator? HexDigit)\* + HexDigit :: one of - `0` `1` `2` `3` `4` `5` `6` `7` `8` `9` @@ -358,9 +428,9 @@ ParseException: Syntax error, unexpected "42" **Static Semantics.** -A {DecimalIntLiteral} whose first digit is `0` and which is longer than a -single character is interpreted in the _legacy octal_ radix (base 8), for -compatibility with historical PHP source. Thus, `042` denotes the value 34. +An {OctalIntLiteral} written without an {OctalIndicator} is the _legacy octal_ +spelling kept for compatibility with historical PHP source. It is read in the +same radix (base 8) the prefixed spelling is, so `042` denotes the value 34. A conforming implementation MUST retain the original (raw) lexeme of every integer literal. When the denoted value exceeds the range representable by the @@ -376,19 +446,19 @@ FloatLiteral :: - TrailingFloatLiteral - ExponentFloatLiteral -LeadingFloatLiteral :: NegativeSign? Digit+ `.` Digit\* ExponentPart? +LeadingFloatLiteral :: Sign? Digits `.` Digits? ExponentPart? -TrailingFloatLiteral :: NegativeSign? Digit\* `.` Digit+ ExponentPart? +TrailingFloatLiteral :: Sign? `.` Digits ExponentPart? -ExponentFloatLiteral :: NegativeSign? Digit+ ExponentPart +ExponentFloatLiteral :: Sign? Digits ExponentPart -ExponentPart :: ExponentIndicator NegativeSign? Digit+ +ExponentPart :: ExponentIndicator Sign? Digits ExponentIndicator :: one of `e` `E` A floating-point literal denotes a value of the PHP `float` type. It MUST contain either a decimal point or an exponent, or both, and MAY be prefixed -with a {NegativeSign}. +with a {Sign}. Either the leading run of digits (before the decimal point) or the trailing run (after it) MAY be omitted, but not both: @@ -405,13 +475,25 @@ run (after it) MAY be omitted, but not both: 1. ``` -Scientific notation uses the case-insensitive `e` indicator followed by an -optionally negative decimal exponent: +Scientific notation uses the case-insensitive `e` indicator followed by a +decimal exponent, which takes a {Sign} of its own: ```typescript 10e-2 ``` +```typescript +10e+2 +``` + +Every run of digits a float is written of accepts the {DigitSeparator} under +the same rule an integer does, and, unlike a {DecimalIntLiteral}, MAY begin +with a zero: + +```typescript +2_3.4_5e-6_7 +``` + **Counter-example.** A lone decimal point is not a valid float. ```typescript counter-example diff --git a/docs/Specification/Section 3 -- Types.md b/docs/Specification/Section 3 -- Types.md index 3090ac6..dabbfe8 100644 --- a/docs/Specification/Section 3 -- Types.md +++ b/docs/Specification/Section 3 -- Types.md @@ -47,7 +47,7 @@ ConditionalOperand : - Type - Variable -ConditionalOperator : one of `is` `is not` `>=` `<=` `<` `>` +ConditionalOperator : one of `is` `is not` A _conditional type_ (also called a ternary type) selects between two types based on a comparison between a _subject_ and a _comparand_. It is written as @@ -58,9 +58,9 @@ condition holds, a `:`, and the type chosen otherwise. T is string ? int : bool ``` -The negative-equality operator is spelled `is not`. At the lexical level, the -two words MUST be separated only by horizontal whitespace and are scanned as a -single token; no line terminator may appear between them. +The negative-equality operator is spelled `is not`. The two words are separate +tokens, so whitespace and comments MAY stand between them the way they MAY +stand between any other two tokens. ```typescript T is not string ? int : bool @@ -77,11 +77,6 @@ $value is array ? non-empty-array : null array is $value ? non-empty-array : null ``` -In addition to `is` and `is not`, the relational operators `<`, `>`, `<=` and -`>=` MAY be used as the condition operator. This is an extension beyond what -PHPStan and Psalm accept; see -[Relationship to Other Tools](#sec-Relationship-to-Other-Tools). - Note: A bare {Variable} is only a valid type when it carries a {ConditionalSuffix}. A {Variable} MUST NOT otherwise stand alone as a type; the only variable-like token that may appear as a primary type on its own is @@ -189,19 +184,16 @@ User[] User[][] ``` -**Counter-example.** The legacy list syntax does not accept a key type between -the brackets; for that, use [offset access](#sec-Offset-Access) or the modern -`array<…>` generic. +The legacy list syntax accepts no key type between the brackets; for that, +use [offset access](#sec-Offset-Access) or the modern `array<…>` generic. A +`User[int]` is a well-formed document all the same — an _offset access_ type +(see below) rather than a list, since `int` is a valid {Type}. It is the +absence of a type between the brackets that selects the list reading: -```typescript counter-example +```typescript User[int] ``` -This document, however, parses `User[int]` successfully — as an -_offset access_ type (see below) rather than a list — because `int` is a -valid {Type}. It is the absence of a type between the brackets that selects -the list interpretation. - ### Offset Access A pair of square brackets enclosing a {Type} forms an _offset access_ type, @@ -275,6 +267,14 @@ that immediately follows it: `*` selects a [generic or shape](#sec-Generic-Types) named type; any other following token (or the end of the document) leaves a plain named type. +Note: A bare {ReservedWord} is likewise both a {LiteralType} and, through +{Identifier}, a {Name} of a single segment (see +[Reserved Words](#sec-Reserved-Words)). Where one source could be read as +either, the {LiteralType} is the one taken, so a `true` denotes the boolean +literal and never a type named `true`. An `is` and a `not` denote no literal +of their own, and nothing is therefore left to shadow the named type each of +them is. + ## Names and Namespaces Name : @@ -307,13 +307,12 @@ Example\Name A separator MAY appear at the start of a name or between two segments, but MUST NOT appear at the end. -**Reserved words within names.** The reserved words `true`, `false`, `null` -and `is` (see [Reserved Words](#sec-Reserved-Words)) MAY appear as an -{Identifier} segment of a {Name}. In isolation, such a word is scanned as a -literal or operator and so cannot stand as a bare type name; but in a -qualified position — following a separator or another segment — it denotes a -name segment. This makes `\null` a reference to a type named `null`, distinct -from the `null` literal. +**Reserved words within names.** Every reserved word — `true`, `false`, +`null`, `is` and `not` (see [Reserved Words](#sec-Reserved-Words)) — MAY +appear as an {Identifier} segment of a {Name}. In a qualified position — +following a separator or another segment — such a word is a name segment and +nothing besides. This makes `\null` a reference to a type named `null`, +distinct from the `null` literal. **Counter-example.** A name cannot end with a separator. @@ -348,13 +347,16 @@ implementation and is outside the scope of this specification. TemplateArguments : `<` TemplateArgument (`,` TemplateArgument)\* `,`? `>` -TemplateArgument : AttributeGroups? (TemplateArgumentHint | TemplateArgumentType) +TemplateArgument : TemplateArgumentHint | TemplateArgumentValue -TemplateArgumentType : Type +TemplateArgumentValue : Wildcard | Type + +Wildcard : `*` A _generic type_ supplies a named type with one or more _template arguments_, -each of which is itself a {Type}. Arguments are enclosed in angle brackets -(`<` and `>`) and separated by commas. A trailing comma is permitted. +each of which is itself a {Type} or a {Wildcard}. Arguments are enclosed in +angle brackets (`<` and `>`) and separated by commas. A trailing comma is +permitted. Validating the number of arguments, their bounds, and their nesting is the responsibility of the implementation, not of this grammar, which imposes no @@ -391,9 +393,9 @@ describes only use sites, and therefore only template arguments. ### Template Argument Hints -TemplateArgumentHint : Identifier Type +TemplateArgumentHint : NameToken TemplateArgumentValue -A template argument MAY carry a single leading _hint_: an {Identifier} placed +A template argument MAY carry a single leading _hint_: a {NameToken} placed before the argument's type. Hints are used by tooling — for example, to express [call-site variance](https://phpstan.org/blog/guide-to-call-site-generic-variance#call-site-variance) with identifiers such as `in`, `out`, `covariant`, or `contravariant`. @@ -402,40 +404,36 @@ with identifiers such as `in`, `out`, `covariant`, or `contravariant`. HashMap ``` -At the lexical level, the hint and the type it modifies MUST be separated by -whitespace; this separation is what distinguishes a hint from the start of -the argument's own type (see [Ignored Tokens](#sec-Ignored-Tokens)). Each -argument may carry at most one hint. +At the lexical level, {Whitespace} MUST stand directly behind the hint; this +separation is what distinguishes a hint from the start of the argument's own +type, and a {Comment} does not stand in its place (see +[Ignored Tokens](#sec-Ignored-Tokens)). Each argument may carry at most one +hint. -**Counter-example.** A hint must be a single valid identifier. +**Counter-example.** A hint is a single {NameToken}. A second one reads as +the bound of a [template parameter](#sec-Callable-Types), and a parameter +list belongs to a callable, so the statement is refused where the `(` it +would go on with is missing. ```typescript counter-example HashMap ``` ``` -ParseException: Syntax error, unexpected "Request" +ParseException: Syntax error, unexpected end of input ``` -**Counter-example.** A hint cannot be a reserved word: `is`, `true`, `false` -and `null` are always scanned as the corresponding operator or literal token -(see [Reserved Words](#sec-Reserved-Words)) and never as a hint {Identifier}. +**Counter-example.** A hint cannot be a reserved word: `true`, `false`, +`null`, `is` and `not` are always scanned as the corresponding literal or +operator token (see [Reserved Words](#sec-Reserved-Words)) and never as a +{NameToken}. ```typescript counter-example HashMap ``` ``` -ParseException: Syntax error, unexpected "Request" -``` - -### Template Argument Attributes - -Each template argument MAY additionally be prefixed with one or more -[attribute groups](#sec-Attributes), providing metadata for the argument. - -```typescript -HashMap<#[name("key")] T, #[name("value")] U> +ParseException: Syntax error, unexpected ">" ``` ## Literal Types @@ -536,49 +534,92 @@ ParseException: Syntax error, unexpected "\" ConstantMask : -- Name `*` -- Name `::` Identifier `*` -- Name `::` `*` +- GlobalConstantMask +- ClassConstantMask + +GlobalConstantMask : + +- Name MaskTail +- Name `\` MaskTail +- LeadingMask -A _constant mask_ denotes a family of constants whose names share a common -prefix. A mask MUST terminate with an asterisk (`*`). +ClassConstantMask : -A global constant mask matches every global constant beginning with the -prefix: +- Name `::` Identifier MaskTail? +- Name `::` MaskTail + +MaskTail : Wildcard (Identifier Wildcard)\* Identifier? + +LeadingMask : Wildcard Identifier MaskTail? + +A _constant mask_ denotes a family of constants whose names are written of the +same segments, in the same order, with a {Wildcard} standing wherever the name +is left unsaid. A mask MUST contain at least one {Wildcard}, and its segments +and wildcards alternate, so two wildcards in a row denote nothing a single one +does not. + +A global constant mask matches every global constant the segments describe: ```typescript JSON_* ``` -A class constant mask matches every constant of a class whose name begins -with the prefix; the prefix MAY be omitted entirely, in which case the mask -matches every constant of the class: +The name of a global constant MAY be left unsaid from its very beginning, and +the namespace it belongs to MAY be written in front of it. A {Name} standing in +front of a {MaskTail} carries the namespace and, in its last segment, the +leading segment of the mask; a {Name} separated from the mask by a `\` is a +namespace whole, and the mask then matches every constant that namespace holds: + +```typescript +*_SUFFIX +``` + +```typescript +Path\To\JSON_* +``` + +```typescript +Path\To\* +``` + +A class constant mask matches every constant of a class the segments describe; +the leading segment MAY be omitted entirely, in which case the mask matches +every constant of the class: ```typescript Path\To\ClassName::PREFIX_* ``` +```typescript +Path\To\ClassName::*_SUFFIX +``` + +```typescript +Path\To\ClassName::PREFIX_*_SUFFIX +``` + ```typescript Path\To\ClassName::* ``` -**Counter-example.** A global mask must have a prefix; a lone asterisk is not -a type. +**Counter-example.** A global mask must have at least one segment of a name, +whether it stands in front of a wildcard or behind it; a lone asterisk, which +would match every constant there is, is not a type. ```typescript counter-example * ``` ``` -ParseException: Syntax error, unexpected "*" +ParseException: Syntax error, unexpected end of input ``` -**Counter-example.** The asterisk must be the final character of the mask. +**Counter-example.** Two wildcards in a row are not a mask. ```typescript counter-example -Path\To\ClassName::PREFIX_*_SUFFIX +Path\To\ClassName::PREFIX_** ``` ``` -ParseException: Syntax error, unexpected "_SUFFIX" +ParseException: Syntax error, unexpected "*" ``` diff --git a/docs/Specification/Section 4 -- Structural Types.md b/docs/Specification/Section 4 -- Structural Types.md index e38ac4e..f6de26f 100644 --- a/docs/Specification/Section 4 -- Structural Types.md +++ b/docs/Specification/Section 4 -- Structural Types.md @@ -1,9 +1,8 @@ # Structural Types -This section defines the structural type forms — _shapes_ and _callables_ — -and the _attribute_ syntax shared by several constructs. Each of these forms -builds on the [named type](#sec-Named-Types) grammar introduced in the -previous section. +This section defines the structural type forms — _shapes_ and _callables_. +Each of these forms builds on the [named type](#sec-Named-Types) grammar +introduced in the previous section. ## Shape Types @@ -45,7 +44,7 @@ array{} ### Shape Fields -ShapeField : AttributeGroups? (ExplicitField | ImplicitField) +ShapeField : ExplicitField | ImplicitField ExplicitField : ShapeKey `?`? `:` ShapeValue @@ -157,24 +156,9 @@ keys — using the same angle-bracket syntax as generics: array{ user: User, ... } ``` -### Shape Field Attributes - -Each shape field MAY be prefixed with one or more -[attribute groups](#sec-Attributes): - -```typescript -App\Domain\User{ - #[name("user_name")] - userName: non-empty-string, - #[skip_when_empty] - friends: list, - ... -} -``` - ## Callable Types -CallableType : Name `(` CallableParameters? `)` CallableReturnType? +CallableType : Name TemplateParameters? `(` CallableParameters? `)` CallableReturnType? CallableParameters : CallableParameter (`,` CallableParameter)\* `,`? @@ -182,8 +166,10 @@ CallableReturnType : `:` Type A _callable type_ describes a function-like value. It is a [name](#sec-Names-and-Namespaces) — commonly `callable` or `Closure`, but any -name is accepted — followed by a parenthesized, possibly empty, parameter -list, and an optional return type introduced by a colon. +name is accepted — optionally followed by the +[template parameters](#sec-Template-Parameters) it declares, then by a +parenthesized, possibly empty, parameter list, and an optional return type +introduced by a colon. ```typescript callable() @@ -197,185 +183,214 @@ callable(): void Closure(int<0, max>, callable(?C): mixed): void ``` -### Callable Parameters +### Template Parameters -CallableParameter : AttributeGroups? CallableParameterBody `=`? +TemplateParameters : `<` TemplateParameter (`,` TemplateParameter)\* `,`? `>` -CallableParameterBody : +TemplateParameter : Identifier TemplateBound\* TemplateDefault? -- `...`? Type ParameterModifiers? Variable? -- ParameterModifiers? Variable +TemplateBound : -ParameterModifiers : +- UpperBound +- LowerBound -- `&` `...`? -- `...` `&`? +UpperBound : UpperBoundOperator Type -A parameter is described by its type, optionally followed by a name. A -parameter MAY also be given by name alone, without a type. +UpperBoundOperator : one of `of` `as` -```typescript -callable(Type) -``` +LowerBound : `super` Type -```typescript -callable(Type $name) -``` +TemplateDefault : `=` Type -```typescript -callable($name) -``` +A callable type MAY declare the _template parameters_ it introduces, written +as a `<...>` list between the name and the parameter list. Each parameter is +an {Identifier} followed by as many _bounds_ as are put on it. -**Named Parameters.** A name beginning with `$` MAY follow the parameter's -type, [permitting the argument to be passed by name](https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments), -exactly as in PHP. +A parameter accepts three kinds of limit, each of which MUST be written at +most once. The two bounds MAY be written in either order, while a +{TemplateDefault} MUST be written last: a bound standing behind it belongs to +the {Type} of the default rather than to the parameter. + +- An {UpperBound} narrows the parameter from above: the argument is to be a + subtype of the given {Type}. Its two operators denote the same thing, and a + conforming implementation MUST retain the one that was written. +- A {LowerBound} bounds the parameter from below: the argument is to be a + supertype of the given {Type}. +- A {TemplateDefault} is the {Type} the parameter takes when no argument is + supplied. It bounds nothing. + +Each operator is matched the one way it is spelled, so an `OF` bounds nothing +an `of` does. ```typescript -callable(A $a, B, C) +callable(T): T ``` -**Output Parameters.** An ampersand (`&`) placed _after_ the parameter type -marks the parameter as passed by reference (an _output_ parameter). +```typescript +callable(T): T +``` ```typescript -callable(T&) +Closure(T, U): V ``` ```typescript -callable(T &$name) +callable(T): void ``` -**Counter-example.** The ampersand must follow the type; it must not precede -it. +**Counter-example.** A word that bounds nothing is not an operator. ```typescript counter-example -callable(&T) +callable(): void ``` ``` -ParseException: Syntax error, unexpected "T" +ParseException: Template parameter cannot be bounded with "whatever", expected one of "of", "as" or "super" ``` -**Optional Parameters.** A trailing `=` marks a parameter as optional: the -caller MAY omit the corresponding argument. +**Counter-example.** Neither is a word that is spelled the way an operator is +spelled, but written in a case of its own. -```typescript -callable(T=) +```typescript counter-example +callable(): void ``` -```typescript -callable(T &$name=) +``` +ParseException: Template parameter cannot be bounded with "OF", expected one of "of", "as" or "super" ``` -**Variadic Parameters.** An ellipsis (`...`) marks a parameter as variadic. It -MAY be written either before the type or after it, immediately before the -name. +**Counter-example.** Each kind of limit is written at most once. -```typescript -callable(...T) +```typescript counter-example +callable(): void ``` -```typescript -callable(T ...$name) ``` - -```typescript -callable(T &...$name) +ParseException: Template parameter cannot have more than one upper bound ``` -**Counter-example.** The ellipsis may appear in only one position; a -parameter that is variadic in both the prefix and suffix positions is an -error. +**Counter-example.** A bound does not stand behind a default. ```typescript counter-example -callable(...T...) +callable(): void ``` ``` -ParseException: Either prefix or postfix variadic syntax should be used, but not both +ParseException: Template parameter default must be written last, since a bound behind it reads as a bound of the default itself ``` -**Counter-example.** A variadic parameter is already optional and therefore -must not additionally carry a default marker. +A `<...>` list is written the same way whether it declares template parameters +or supplies [template arguments](#sec-Generic-Types), and only the `(` that +follows a parameter list tells them apart. A list that no `(` follows is +therefore a list of template arguments, which describes no bounds. + +**Counter-example.** Bounds belong to a callable alone. ```typescript counter-example -callable(T ...$name=) +Collection ``` ``` -ParseException: Cannot have variadic param with a default +ParseException: Syntax error, unexpected end of input ``` +### Callable Parameters + +CallableParameter : Type `&`? `...`? Variable? `=`? -### Callable Parameter Attributes +A parameter is described by its type, optionally followed by the reference +and the variadic markers, by a name and by the default marker — in that +order and in no other. The type is the only required part. -Each callable parameter MAY be prefixed with one or more -[attribute groups](#sec-Attributes): +```typescript +callable(Type) +``` ```typescript -Example\Functor(#[type] int $a): void +callable(Type $name) ``` -## Attributes +**Counter-example.** A parameter given by name alone, without a type, is +not permitted. -AttributeGroups : AttributeGroup+ +```typescript counter-example +callable($name) +``` -AttributeGroup : `#[` AttributeList `,`? `]` +``` +ParseException: Syntax error, unexpected ")" +``` -AttributeList : Attribute (`,` Attribute)\* +**Named Parameters.** A name beginning with `$` MAY follow the parameter's +type, [permitting the argument to be passed by name](https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments), +exactly as in PHP. -Attribute : Name AttributeArguments? +```typescript +callable(A $a, B, C) +``` -AttributeArguments : `(` AttributeArgument (`,` AttributeArgument)\* `,`? `)` +**Output Parameters.** An ampersand (`&`) placed _after_ the parameter type +marks the parameter as passed by reference (an _output_ parameter). -AttributeArgument : Type +```typescript +callable(T&) +``` -An _attribute_ attaches arbitrary, implementation-defined metadata to the -construct it precedes. The syntax mirrors -[PHP attributes](https://www.php.net/manual/en/language.attributes.php): each -attribute is a [name](#sec-Names-and-Namespaces) optionally followed by a -parenthesized list of arguments, and each argument is itself an arbitrary -{Type} — including, recursively, a type that carries attributes of its own. +```typescript +callable(T &$name) +``` -Attributes are written in groups delimited by `#[` and `]`. A construct MAY -carry several attributes within one group, separated by commas, and several -groups in sequence. Attributes may be attached to a -[template argument](#sec-Template-Argument-Attributes), a -[shape field](#sec-Shape-Field-Attributes), or a -[callable parameter](#sec-Callable-Parameter-Attributes). +**Counter-example.** The ampersand must follow the type; it must not precede +it. -A single attribute with one argument: +```typescript counter-example +callable(&T) +``` -```typescript -Example\Functor(#[type] int $a): void ``` +ParseException: Syntax error, unexpected "&" +``` + +**Optional Parameters.** A trailing `=` marks a parameter as optional: the +caller MAY omit the corresponding argument. -Several attributes within one group: +```typescript +callable(T=) +``` ```typescript -HashMap<#[name("key"), out] T> +callable(T &$name=) ``` -Several groups in sequence: +**Variadic Parameters.** An ellipsis (`...`) placed after the parameter type +marks the parameter as variadic. Where a parameter carries both markers, the +ampersand comes first. ```typescript -array{ - #[serialize("onSerialize")] - #[deserialize("onDeserialize")] - test?: App\Domain\User, -} +callable(T ...$name) ``` -**Counter-example.** An attribute name must be a valid {Name}; a literal is -not permitted in that position. +```typescript +callable(T &...$name) +``` + +**Counter-example.** The ellipsis must follow the type; it must not precede +it. ```typescript counter-example -Collection<#[42] User> +callable(...T) ``` ``` -ParseException: Syntax error, unexpected "42" +ParseException: Syntax error, unexpected "..." ``` -Note: Because the `#[` attribute marker is scanned as a single token by the -[lexical grammar](#sec-Comments), it is never confused with the `#` -line-comment marker. +**Counter-example.** A variadic parameter is already optional and therefore +must not additionally carry a default marker. + +```typescript counter-example +callable(T ...$name=) +``` + +``` +ParseException: Cannot have variadic param with a default +``` diff --git a/docs/Writerside/cfg/static/spec.html b/docs/Writerside/cfg/static/spec.html index 4ad11cd..09f6a23 100644 --- a/docs/Writerside/cfg/static/spec.html +++ b/docs/Writerside/cfg/static/spec.html @@ -74,7 +74,6 @@
Statu
  • 5.7Named Types
  • 5.8Generic Types
    1. 5.8.1Template Argument Hints
    2. -
    3. 5.8.2Template Argument Attributes
  • 5.9Literal Types
  • @@ -91,15 +90,13 @@
    Statu
  • 6.1.1Shape Fields
  • 6.1.2Optional Fields
  • 6.1.3Unsealed Shapes
  • -
  • 6.1.4Shape Field Attributes
  • 6.2Callable Types
      -
    1. 6.2.1Callable Parameters
    2. -
    3. 6.2.2Callable Parameter Attributes
    4. +
    5. 6.2.1Template Parameters
    6. +
    7. 6.2.2Callable Parameters
  • -
  • 6.3Attributes
  • AAppendix: Notation Conventions
      @@ -176,7 +173,6 @@

      nopqrstuvwxyz -Any byte from U+0080 to U+00FF +Any byte from 0x80 to 0xFF

      Ignored tokens improve readability and separate lexical tokens, but are otherwise insignificant. Any amount of Ignored MAY appear before and after every lexical Token.

      -
      -Note -Although Ignored between two tokens is never itself significant, the presence of separating whitespace is what distinguishes a NameToken that is immediately followed by another token from a NameToken that introduces a template argument hint (see Template Argument Hints). This is the only place in the grammar where the presence of separating whitespace is observable to the syntactic grammar.
      +
      +Note +Although Ignored between two tokens is never itself significant, the presence of Whitespace standing directly behind a NameToken is what distinguishes a NameToken that is immediately followed by another token from a NameToken that introduces a template argument hint (see Template Argument Hints). This is the only place in the grammar where Ignored is observable to the syntactic grammar, and the one place a Comment does not stand for the whitespace it is written among: a comment MAY follow that whitespace, but MUST NOT stand in its stead.

      4.2.1White Space

      -Whitespace
      Any Unicode whitespace character
      +Whitespace
      + + + + + + + + + + + + +
      Horizontal Tab (U+0009)
      Line Feed (U+000A)
      Vertical Tab (U+000B)
      Form Feed (U+000C)
      Carriage Return (U+000D)
      Space (U+0020)
      -

      Whitespace separates tokens and improves the legibility of the source text. It includes, at minimum, the space (U+0020), horizontal tab (U+0009), line feed (U+000A) and carriage return (U+000D) characters. TypeLang does not distinguish between horizontal whitespace and line terminators, except within the multi-word is not operator (see Conditional Types).

      +

      Whitespace separates tokens and improves the legibility of the source text. TypeLang does not distinguish between horizontal whitespace and line terminators, and a Comment stands wherever whitespace stands, save for the one place the presence of the whitespace itself is read (see Ignored Tokens).

      +
      +Note +The six characters above are the whole of it. A source text is read as a sequence of bytes (see Source Text), so a character that parts words in a script of its own — a no-break space (U+00A0), say — is no whitespace here: every byte it is written of falls in the range a Letter covers, and it is read as a part of the name it stands in.

      4.2.2Comments

      @@ -284,9 +296,6 @@

      LineTerminator (or the end of the source, whichever occurs first). A block comment begins with /* and continues up to and including the next */.

      Comments are Ignored and have no bearing on the meaning of a document.

      -
      -Note -The # line comment marker and the #[ attribute marker share a leading character. Because lexical analysis prefers the longest match, the sequence #[ is always scanned as the start of an attribute (see Attributes) rather than as the start of a comment.

      @@ -311,12 +320,12 @@

      ()[]{} -<><=>= +<> -::\...#[ +::\... -

      TypeLang documents use punctuation to describe structure. Several punctuators share a leading character (for example, < and <=; : and ::; . in ...; # in #[); in every such case, the longest matching punctuator is taken.

      +

      TypeLang documents use punctuation to describe structure. Several punctuators share a leading character (for example, : and ::; . in ...); in every such case, the longest matching punctuator is taken.

      4.3.2Names

      @@ -334,6 +343,9 @@

      4
      -

      A NameToken is the raw lexical unit that is later assembled, together with the namespace separator, into the syntactic Name production (see Names and Namespaces); the two are distinct non-terminals of, respectively, the lexical and the syntactic grammar. A NameToken MUST begin with a Letter or an underscore (_) and MAY continue with any Letter, Digit, underscore, or dash (-). The only difference from the PHP identifier grammar is that the dash character is additionally permitted in any non-leading position.

      +

      A source text is read as a sequence of bytes rather than of codepoints, and every byte of a character outside of ASCII falls in the 0x80 to 0xFF range a Letter covers. A name MAY therefore be written in any script.

      +
      Проект\Тип
      +

      A NameToken is always the longest possible valid sequence; it MUST NOT be followed by a NameContinue character.

      ExampleTypeName
       
      @@ -354,25 +366,33 @@

      ReservedWord
      - +
      truefalsenullis
      truefalsenullisnot
      -

      The words true, false and null are literal keywords (see Literal Tokens), and is is the conditional operator keyword (see Conditional Types). All four are matched case-insensitively, and only when not immediately followed by a NameContinue character.

      -

      When the source contains one of these words standing alone, in a position where a type is expected, it is scanned as the corresponding literal or operator token and MUST NOT be scanned as a NameToken. Consequently, a bare reserved word cannot be used as a type name.

      -

      Counter-example. A bare reserved word is scanned as a literal, not a type name.

      -
      Counter Example № 2TrUe
      +

      The words true, false and null are literal keywords (see Literal Tokens), and is and not are the conditional operator keywords (see Conditional Types). Each is matched only when not immediately followed by a NameContinue character. The three literal keywords are matched case-insensitively, while is and not are matched the one way they are spelled.

      +

      Wherever the source contains one of these words, it is scanned as the corresponding literal or operator token and MUST NOT be scanned as a NameToken. It remains an Identifier all the same (see Names and Namespaces), so a reserved word is shadowed rather than forbidden: it stands as a name wherever the token it is scanned as denotes no type of its own.

      +

      A literal keyword standing alone, in a position where a type is expected, is read as the LiteralType it denotes and not as the NamedType it would otherwise be (see Primary Types):

      +
      TrUe
      +
      +

      Behind a namespace separator or another segment, where no literal is expected, the same word is a name segment. This makes \true a reference to a type literally named true, distinct from the true literal:

      +
      \true
      +
      +

      The conditional operator keywords denote no type of their own, so nothing shadows them, and a bare is or not is an ordinary named type:

      +
      not
       
      -

      A reserved word MAY, however, appear as an Identifier inside a qualified Name — that is, when it is preceded by a namespace separator or another identifier (see Names and Namespaces). For example, \true references a type literally named true, whereas the bare true is the boolean literal.

      4.3.4Variable

      + -

      A Variable token begins with a dollar sign ($) followed by a sequence matching the body of a NameToken. Variables are used to name callable parameters (see Callable Types) and as operands in conditional types (see Conditional Types).

      +

      A Variable token begins with a dollar sign ($) followed by a sequence matching the body of a NameToken, save for the dash a variable cannot carry. Variables are used to name callable parameters (see Callable Types) and as operands in conditional types (see Conditional Types).

      The special variable $this is recognised as a distinct token and additionally denotes the current object type when used as a primary type (see Primary Types).

      Note @@ -410,20 +430,35 @@

      NegativeSign
      -

      +
      +Sign
      + + +
      -+
      +
      -

      An integer literal denotes a value of the PHP int type. Binary, octal, decimal and hexadecimal radixes are supported, each optionally prefixed with a NegativeSign. Within the digits of any integer literal, underscores (DigitSeparator) MAY appear freely as visual separators; they carry no meaning and do not affect the denoted value.

      -
      -
      Decimal.
      +

      An integer literal denotes a value of the PHP int type. Binary, octal, decimal and hexadecimal radixes are supported, each optionally prefixed with a Sign. A NegativeSign makes the denoted value negative; a leading + denotes the same value the absence of a Sign does. Underscores (DigitSeparator) MAY appear as visual separators; they carry no meaning and do not affect the denoted value. A DigitSeparator MUST stand between two digits, and therefore may neither lead a run of digits, nor trail one, nor stand beside another DigitSeparator.

      +
      +Digits
      Digit(DigitSeparator?Digit)*
      +
      +

      Decimal. A decimal literal other than a lone 0 MUST NOT begin with a zero, which is the mark of an OctalIntLiteral.

      +
      +NonZeroDigit
      + + +
      123456789
      1_000_000
       

      Binary. Prefixed with 0b or 0B; digits are 0 and 1 only.

      BinaryIndicator
      @@ -431,6 +466,9 @@
      Decimal.
      bB
      +
      BinaryDigit
      @@ -439,9 +477,10 @@
      Decimal.
      0b1010_1101
       
      -

      Octal. Prefixed with 0o or 0O; digits are 0 through 7 only.

      +

      Octal. Prefixed with 0o or 0O or, as octal has been written since long before that prefix, with a leading 0 alone; digits are 0 through 7 only. A 0123 denotes the same value a 0o123 does.

      @@ -449,6 +488,9 @@
      Decimal.
      oO
      +
      +OctalDigits
      OctalDigit(DigitSeparator?OctalDigit)*
      +
      OctalDigit
      @@ -457,9 +499,16 @@
      Decimal.
      0o42
       
      +
      04_23
      +
      +

      Counter-example. A leading zero makes a literal an octal, so a digit outside the octal radix may not follow one.

      +
      Counter Example № 208
      +
      +
      ParseException: Syntax error, unexpected "8"
      +

      Hexadecimal. Prefixed with 0x or 0X; digits are 0 through 9 and a through f, in either case.

      @@ -467,6 +516,9 @@
      Decimal.
      xX
      +
      +HexDigits
      HexDigit(DigitSeparator?HexDigit)*
      +
      HexDigit
      @@ -484,10 +536,9 @@
      Decimal.
      ParseException: Syntax error, unexpected "42"
       
      -
      Static Semantics.
      -

      A DecimalIntLiteral whose first digit is 0 and which is longer than a single character is interpreted in the legacy octal radix (base 8), for compatibility with historical PHP source. Thus, 042 denotes the value 34.

      +

      An OctalIntLiteral written without an OctalIndicator is the legacy octal spelling kept for compatibility with historical PHP source. It is read in the same radix (base 8) the prefixed spelling is, so 042 denotes the value 34.

      A conforming implementation MUST retain the original (raw) lexeme of every integer literal. When the denoted value exceeds the range representable by the implementation’s platform integer type, the numeric value MUST be clamped to the nearest representable bound (the platform’s PHP_INT_MAX or PHP_INT_MIN) while the raw lexeme MUST be preserved unchanged.

      @@ -499,16 +550,16 @@

      4

      @@ -516,7 +567,7 @@

      4

      eE
      -

      A floating-point literal denotes a value of the PHP float type. It MUST contain either a decimal point or an exponent, or both, and MAY be prefixed with a NegativeSign.

      +

      A floating-point literal denotes a value of the PHP float type. It MUST contain either a decimal point or an exponent, or both, and MAY be prefixed with a Sign.

      Either the leading run of digits (before the decimal point) or the trailing run (after it) MAY be omitted, but not both:

      0.9
       
      @@ -524,9 +575,14 @@

      4
      1.
       
      -

      Scientific notation uses the case-insensitive e indicator followed by an optionally negative decimal exponent:

      +

      Scientific notation uses the case-insensitive e indicator followed by a decimal exponent, which takes a Sign of its own:

      10e-2
       
      +
      10e+2
      +
      +

      Every run of digits a float is written of accepts the DigitSeparator under the same rule an integer does, and, unlike a DecimalIntLiteral, MAY begin with a zero:

      +
      2_3.4_5e-6_7
      +

      Counter-example. A lone decimal point is not a valid float.

      Counter Example № 4.
       
      @@ -679,13 +735,13 @@

      ConditionalOperator
      - +
      isis not>=<=<>
      isis not

      A conditional type (also called a ternary type) selects between two types based on a comparison between a subject and a comparand. It is written as the subject, an operator, the comparand, a ?, the type chosen when the condition holds, a :, and the type chosen otherwise.

      T is string ? int : bool
       
      -

      The negative-equality operator is spelled is not. At the lexical level, the two words MUST be separated only by horizontal whitespace and are scanned as a single token; no line terminator may appear between them.

      +

      The negative-equality operator is spelled is not. The two words are separate tokens, so whitespace and comments MAY stand between them the way they MAY stand between any other two tokens.

      T is not string ? int : bool
       

      Either the subject or the comparand MAY be a Variable instead of a Type, including in “Yoda” order, where the variable appears on the right:

      @@ -693,7 +749,6 @@

      array is $value ? non-empty-array : null -

      In addition to is and is not, the relational operators <, >, <= and >= MAY be used as the condition operator. This is an extension beyond what PHPStan and Psalm accept; see Relationship to Other Tools.

      Note A bare Variable is only a valid type when it carries a ConditionalSuffix. A Variable MUST NOT otherwise stand alone as a type; the only variable-like token that may appear as a primary type on its own is $this (see Primary Types).
      @@ -771,10 +826,9 @@

      User[][] -

      Counter-example. The legacy list syntax does not accept a key type between the brackets; for that, use offset access or the modern array<…> generic.

      -
      Counter Example № 6User[int]
      +

      The legacy list syntax accepts no key type between the brackets; for that, use offset access or the modern array<…> generic. A User[int] is a well-formed document all the same — an offset access type (see below) rather than a list, since int is a valid Type. It is the absence of a type between the brackets that selects the list reading:

      +
      User[int]
       
      -

      This document, however, parses User[int] successfully — as an offset access type (see below) rather than a list — because int is a valid Type. It is the absence of a type between the brackets that selects the list interpretation.

      5.4.2Offset Access

      @@ -789,7 +843,7 @@

      T<U>[object{key: int, ...}]

      Counter-example. Each offset is enclosed in a single pair of brackets.

      -
      Counter Example № 7Collection[[Some]]
      +
      Counter Example № 6Collection[[Some]]
       
      ParseException: Syntax error, unexpected "["
       
      @@ -814,6 +868,9 @@

      Note Each of CallableType, NamedType, and the ClassConstant and ConstantMask forms of LiteralType begins with a Name. Once a complete Name has been recognized, these four are distinguished by the single token that immediately follows it: * selects a constant mask; :: selects a class constant; ( selects a callable type; < or { selects a generic or shape named type; any other following token (or the end of the document) leaves a plain named type. +
      +Note +A bare ReservedWord is likewise both a LiteralType and, through Identifier, a Name of a single segment (see Reserved Words). Where one source could be read as either, the LiteralType is the one taken, so a true denotes the boolean literal and never a type named true. An is and a not denote no literal of their own, and nothing is therefore left to shadow the named type each of them is.

      5.6Names and Namespaces

      @@ -837,9 +894,9 @@

      \Absolute\Type\Name

      A separator MAY appear at the start of a name or between two segments, but MUST NOT appear at the end.

      -

      Reserved words within names. The reserved words true, false, null and is (see Reserved Words) MAY appear as an Identifier segment of a Name. In isolation, such a word is scanned as a literal or operator and so cannot stand as a bare type name; but in a qualified position — following a separator or another segment — it denotes a name segment. This makes \null a reference to a type named null, distinct from the null literal.

      +

      Reserved words within names. Every reserved word — true, false, null, is and not (see Reserved Words) — MAY appear as an Identifier segment of a Name. In a qualified position — following a separator or another segment — such a word is a name segment and nothing besides. This makes \null a reference to a type named null, distinct from the null literal.

      Counter-example. A name cannot end with a separator.

      -
      Counter Example № 8Example\Name\
      +
      Counter Example № 7Example\Name\
       
      ParseException: Syntax error, unexpected end of input
       
      @@ -860,12 +917,15 @@

      TemplateArguments + -
      -TemplateArgumentType + -

      A generic type supplies a named type with one or more template arguments, each of which is itself a Type. Arguments are enclosed in angle brackets (< and >) and separated by commas. A trailing comma is permitted.

      +

      A generic type supplies a named type with one or more template arguments, each of which is itself a Type or a Wildcard. Arguments are enclosed in angle brackets (< and >) and separated by commas. A trailing comma is permitted.

      Validating the number of arguments, their bounds, and their nesting is the responsibility of the implementation, not of this grammar, which imposes no such limits.

      Path\To\ExampleClass<T, U>
       
      @@ -874,7 +934,7 @@

      HashMap<Request, User,>

      Counter-example. At least one argument is required, and a leading comma is not permitted.

      -
      Counter Example № 9example<>
      +
      Counter Example № 8example<>
       
      ParseException: Syntax error, unexpected ">"
       
      @@ -884,27 +944,21 @@

      5.8.1Template Argument Hints

      -

      A template argument MAY carry a single leading hint: an Identifier placed before the argument’s type. Hints are used by tooling — for example, to express call-site variance with identifiers such as in, out, covariant, or contravariant.

      +

      A template argument MAY carry a single leading hint: a NameToken placed before the argument’s type. Hints are used by tooling — for example, to express call-site variance with identifiers such as in, out, covariant, or contravariant.

      HashMap<array-key, covariant Request>
       
      -

      At the lexical level, the hint and the type it modifies MUST be separated by whitespace; this separation is what distinguishes a hint from the start of the argument’s own type (see Ignored Tokens). Each argument may carry at most one hint.

      -

      Counter-example. A hint must be a single valid identifier.

      -
      Counter Example № 10HashMap<array-key, some covariant Request>
      +

      At the lexical level, Whitespace MUST stand directly behind the hint; this separation is what distinguishes a hint from the start of the argument’s own type, and a Comment does not stand in its place (see Ignored Tokens). Each argument may carry at most one hint.

      +

      Counter-example. A hint is a single NameToken. A second one reads as the bound of a template parameter, and a parameter list belongs to a callable, so the statement is refused where the ( it would go on with is missing.

      +
      Counter Example № 9HashMap<array-key, some covariant Request>
       
      -
      ParseException: Syntax error, unexpected "Request"
      -
      -

      Counter-example. A hint cannot be a reserved word: is, true, false and null are always scanned as the corresponding operator or literal token (see Reserved Words) and never as a hint Identifier.

      -
      Counter Example № 11HashMap<is Request>
      +
      ParseException: Syntax error, unexpected end of input
       
      -
      ParseException: Syntax error, unexpected "Request"
      +

      Counter-example. A hint cannot be a reserved word: true, false, null, is and not are always scanned as the corresponding literal or operator token (see Reserved Words) and never as a NameToken.

      +
      Counter Example № 10HashMap<is Request>
       
      -

      -
      -

      5.8.2Template Argument Attributes

      -

      Each template argument MAY additionally be prefixed with one or more attribute groups, providing metadata for the argument.

      -
      HashMap<#[name("key")] T, #[name("value")] U>
      +
      ParseException: Syntax error, unexpected ">"
       
      @@ -954,7 +1008,7 @@

      Path\To\ClassName::ANOTHER_CONSTANT_NAME

      Counter-example. The constant part may not contain a namespace separator.

      -
      Counter Example № 12ClassName::SOME\ANY
      +
      Counter Example № 11ClassName::SOME\ANY
       
      ParseException: Syntax error, unexpected "\"
       
      @@ -962,35 +1016,60 @@

      5.10.3Constant Masks

      + + -

      A constant mask denotes a family of constants whose names share a common prefix. A mask MUST terminate with an asterisk (*).

      -

      A global constant mask matches every global constant beginning with the prefix:

      + + +

      A constant mask denotes a family of constants whose names are written of the same segments, in the same order, with a Wildcard standing wherever the name is left unsaid. A mask MUST contain at least one Wildcard, and its segments and wildcards alternate, so two wildcards in a row denote nothing a single one does not.

      +

      A global constant mask matches every global constant the segments describe:

      JSON_*
       
      -

      A class constant mask matches every constant of a class whose name begins with the prefix; the prefix MAY be omitted entirely, in which case the mask matches every constant of the class:

      +

      The name of a global constant MAY be left unsaid from its very beginning, and the namespace it belongs to MAY be written in front of it. A Name standing in front of a MaskTail carries the namespace and, in its last segment, the leading segment of the mask; a Name separated from the mask by a \ is a namespace whole, and the mask then matches every constant that namespace holds:

      +
      *_SUFFIX
      +
      +
      Path\To\JSON_*
      +
      +
      Path\To\*
      +
      +

      A class constant mask matches every constant of a class the segments describe; the leading segment MAY be omitted entirely, in which case the mask matches every constant of the class:

      Path\To\ClassName::PREFIX_*
       
      +
      Path\To\ClassName::*_SUFFIX
      +
      +
      Path\To\ClassName::PREFIX_*_SUFFIX
      +
      Path\To\ClassName::*
       
      -

      Counter-example. A global mask must have a prefix; a lone asterisk is not a type.

      -
      Counter Example № 13*
      +

      Counter-example. A global mask must have at least one segment of a name, whether it stands in front of a wildcard or behind it; a lone asterisk, which would match every constant there is, is not a type.

      +
      Counter Example № 12*
       
      -
      ParseException: Syntax error, unexpected "*"
      +
      ParseException: Syntax error, unexpected end of input
       
      -

      Counter-example. The asterisk must be the final character of the mask.

      -
      Counter Example № 14Path\To\ClassName::PREFIX_*_SUFFIX
      +

      Counter-example. Two wildcards in a row are not a mask.

      +
      Counter Example № 13Path\To\ClassName::PREFIX_**
       
      -
      ParseException: Syntax error, unexpected "_SUFFIX"
      +
      ParseException: Syntax error, unexpected "*"
       

      6Structural Types

      -

      This section defines the structural type forms — shapes and callables — and the attribute syntax shared by several constructs. Each of these forms builds on the named type grammar introduced in the previous section.

      +

      This section defines the structural type forms — shapes and callables. Each of these forms builds on the named type grammar introduced in the previous section.

      6.1Shape Types

      Mixed keys are not permitted. A single shape MUST use either explicit keys throughout or implicit keys throughout.

      -
      Counter Example № 15array{ named: First, Second }
      +
      Counter Example № 14array{ named: First, Second }
       
      ParseException: Cannot mix explicit and implicit shape keys
       

      Duplicate explicit keys are not permitted. No two explicit fields of the same shape may denote the same key, regardless of which of the five key forms each uses.

      -
      Counter Example № 16array{ 1: int, 2: int, 1: string }
      +
      Counter Example № 15array{ 1: int, 2: int, 1: string }
       
      ParseException: Duplicate key "1"
       
      @@ -1086,23 +1165,11 @@

      array{ user: User, ...<string, object> }

      -
      -

      6.1.4Shape Field Attributes

      -

      Each shape field MAY be prefixed with one or more attribute groups:

      -
      App\Domain\User{
      -    #[name("user_name")]
      -    userName: non-empty-string,
      -    #[skip_when_empty]
      -    friends: list<App\Domain\User>,
      -    ...
      -}
      -
      -

      6.2Callable Types

      -

      A callable type describes a function-like value. It is a name — commonly callable or Closure, but any name is accepted — followed by a parenthesized, possibly empty, parameter list, and an optional return type introduced by a colon.

      +

      A callable type describes a function-like value. It is a name — commonly callable or Closure, but any name is accepted — optionally followed by the template parameters it declares, then by a parenthesized, possibly empty, parameter list, and an optional return type introduced by a colon.

      callable()
       
      callable(): void
       
      Closure(int<0, max>, callable(?C): mixed): void
       
      -
      -

      6.2.1Callable Parameters

      -
      -CallableParameter +
      +

      6.2.1Template Parameters

      + + + -
      -CallableParameterBody - + -
      -ParameterModifiers
      &...opt
      -
      ...&opt
      +
      +UpperBoundOperator
      + + +
      ofas
      +
      + + +

      A callable type MAY declare the template parameters it introduces, written as a <...> list between the name and the parameter list. Each parameter is an Identifier followed by as many bounds as are put on it.

      +

      A parameter accepts three kinds of limit, each of which MUST be written at most once. The two bounds MAY be written in either order, while a TemplateDefault MUST be written last: a bound standing behind it belongs to the Type of the default rather than to the parameter.

      +
        +
      • An UpperBound narrows the parameter from above: the argument is to be a subtype of the given Type. Its two operators denote the same thing, and a conforming implementation MUST retain the one that was written.
      • +
      • A LowerBound bounds the parameter from below: the argument is to be a supertype of the given Type.
      • +
      • A TemplateDefault is the Type the parameter takes when no argument is supplied. It bounds nothing.
      • +
      +

      Each operator is matched the one way it is spelled, so an OF bounds nothing an of does.

      +
      callable<T>(T): T
      +
      +
      callable<T of Some>(T): T
      +
      +
      Closure<T of Some, U super Any, V = int>(T, U): V
      +
      +
      callable<T of Some super Any = int>(T): void
      +
      +

      Counter-example. A word that bounds nothing is not an operator.

      +
      Counter Example № 16callable<T whatever Some>(): void
      +
      +
      ParseException: Template parameter cannot be bounded with "whatever", expected one of "of", "as" or "super"
      +
      +

      Counter-example. Neither is a word that is spelled the way an operator is spelled, but written in a case of its own.

      +
      Counter Example № 17callable<T OF Some>(): void
      +
      +
      ParseException: Template parameter cannot be bounded with "OF", expected one of "of", "as" or "super"
      +
      +

      Counter-example. Each kind of limit is written at most once.

      +
      Counter Example № 18callable<T of Some as Any>(): void
      +
      +
      ParseException: Template parameter cannot have more than one upper bound
      +
      +

      Counter-example. A bound does not stand behind a default.

      +
      Counter Example № 19callable<T = int of Some>(): void
      +
      +
      ParseException: Template parameter default must be written last, since a bound behind it reads as a bound of the default itself
      +
      +

      A <...> list is written the same way whether it declares template parameters or supplies template arguments, and only the ( that follows a parameter list tells them apart. A list that no ( follows is therefore a list of template arguments, which describes no bounds.

      +

      Counter-example. Bounds belong to a callable alone.

      +
      Counter Example № 20Collection<T of Some>
      +
      +
      ParseException: Syntax error, unexpected end of input
      +
      +
      +
      +

      6.2.2Callable Parameters

      +
      +CallableParameter
      Type&opt...optVariableopt=opt
      -

      A parameter is described by its type, optionally followed by a name. A parameter MAY also be given by name alone, without a type.

      +

      A parameter is described by its type, optionally followed by the reference and the variadic markers, by a name and by the default marker — in that order and in no other. The type is the only required part.

      callable(Type)
       
      callable(Type $name)
       
      -
      callable($name)
      +

      Counter-example. A parameter given by name alone, without a type, is not permitted.

      +
      Counter Example № 21callable($name)
      +
      +
      ParseException: Syntax error, unexpected ")"
       

      Named Parameters. A name beginning with $ MAY follow the parameter’s type, permitting the argument to be passed by name, exactly as in PHP.

      callable(A $a, B, C)
      @@ -1146,83 +1278,31 @@ 

      callable(T &$name)

      Counter-example. The ampersand must follow the type; it must not precede it.

      -
      Counter Example № 17callable(&T)
      +
      Counter Example № 22callable(&T)
       
      -
      ParseException: Syntax error, unexpected "T"
      +
      ParseException: Syntax error, unexpected "&"
       

      Optional Parameters. A trailing = marks a parameter as optional: the caller MAY omit the corresponding argument.

      callable(T=)
       
      callable(T &$name=)
       
      -

      Variadic Parameters. An ellipsis (...) marks a parameter as variadic. It MAY be written either before the type or after it, immediately before the name.

      -
      callable(...T)
      -
      +

      Variadic Parameters. An ellipsis (...) placed after the parameter type marks the parameter as variadic. Where a parameter carries both markers, the ampersand comes first.

      callable(T ...$name)
       
      callable(T &...$name)
       
      -

      Counter-example. The ellipsis may appear in only one position; a parameter that is variadic in both the prefix and suffix positions is an error.

      -
      Counter Example № 18callable(...T...)
      +

      Counter-example. The ellipsis must follow the type; it must not precede it.

      +
      Counter Example № 23callable(...T)
       
      -
      ParseException: Either prefix or postfix variadic syntax should be used, but not both
      +
      ParseException: Syntax error, unexpected "..."
       

      Counter-example. A variadic parameter is already optional and therefore must not additionally carry a default marker.

      -
      Counter Example № 19callable(T ...$name=)
      +
      Counter Example № 24callable(T ...$name=)
       
      ParseException: Cannot have variadic param with a default
       
      -
      -

      6.2.2Callable Parameter Attributes

      -

      Each callable parameter MAY be prefixed with one or more attribute groups:

      -
      Example\Functor(#[type<int8>] int $a): void
      -
      -
      -
      -
      -

      6.3Attributes

      - - - - - - -

      An attribute attaches arbitrary, implementation-defined metadata to the construct it precedes. The syntax mirrors PHP attributes: each attribute is a name optionally followed by a parenthesized list of arguments, and each argument is itself an arbitrary Type — including, recursively, a type that carries attributes of its own.

      -

      Attributes are written in groups delimited by #[ and ]. A construct MAY carry several attributes within one group, separated by commas, and several groups in sequence. Attributes may be attached to a template argument, a shape field, or a callable parameter.

      -

      A single attribute with one argument:

      -
      Example\Functor(#[type<int8>] int $a): void
      -
      -

      Several attributes within one group:

      -
      HashMap<#[name("key"), out] T>
      -
      -

      Several groups in sequence:

      -
      array{
      -    #[serialize("onSerialize")]
      -    #[deserialize("onDeserialize")]
      -    test?: App\Domain\User,
      -}
      -
      -

      Counter-example. An attribute name must be a valid Name; a literal is not permitted in that position.

      -
      Counter Example № 20Collection<#[42] User>
      -
      -
      ParseException: Syntax error, unexpected "42"
      -
      -
      -Note -Because the #[ attribute marker is scanned as a single token by the lexical grammar, it is never confused with the # line-comment marker.
      @@ -1325,7 +1405,7 @@
      nopqrstuvwxyz -Any byte from U+0080 to U+00FF +Any byte from 0x80 to 0xFF
      -Whitespace
      Any Unicode whitespace character
      +Whitespace
      + + + + + + + + + + + + +
      Horizontal Tab (U+0009)
      Line Feed (U+000A)
      Vertical Tab (U+000B)
      Form Feed (U+000C)
      Carriage Return (U+000D)
      Space (U+0020)
      LineTerminator
      @@ -1392,9 +1485,9 @@
      (
      - + - +
      )[]{}
      <><=>=
      <>
      ::\...#[
      ::\...
      @@ -1413,14 +1506,17 @@
      ReservedWord
      - +
      truefalsenullis
      truefalsenullisnot
      +
      @@ -1437,9 +1533,18 @@
      NegativeSign
      -
      +
      +Sign
      + + +
      -+
      +
      +
      +Digits
      Digit(DigitSeparator?Digit)*
      +
      +
      +NonZeroDigit
      + + +
      123456789
      BinaryIndicator
      @@ -1458,6 +1570,9 @@
      b
      B
      + + +
      +HexDigits
      HexDigit(DigitSeparator?HexDigit)*
      +
      @@ -1653,13 +1778,16 @@
      TemplateArguments + - + + + +
      @@ -1695,7 +1837,7 @@
      ShapeFieldList
      -
      -
      Attributes
      -
      @@ -1766,24 +1904,19 @@
      §Index
        -
      1. Attribute
      2. -
      3. AttributeArgument
      4. -
      5. AttributeArguments
      6. -
      7. AttributeGroup
      8. -
      9. AttributeGroups
      10. -
      11. AttributeList
      12. BinaryDigit
      13. +
      14. BinaryDigits
      15. BinaryIndicator
      16. BinaryIntLiteral
      17. BlockComment
      18. BlockCommentChar
      19. BoolLiteral
      20. CallableParameter
      21. -
      22. CallableParameterBody
      23. CallableParameters
      24. CallableReturnType
      25. CallableType
      26. ClassConstant
      27. +
      28. ClassConstantMask
      29. Comment
      30. CommentChar
      31. ConditionalOperand
      32. @@ -1793,6 +1926,7 @@

      33. ConstantMask
      34. DecimalIntLiteral
      35. Digit
      36. +
      37. Digits
      38. DigitSeparator
      39. Document
      40. DoubleQuotedString
      41. @@ -1805,7 +1939,9 @@

      42. Expression
      43. FloatLiteral
      44. FullyQualifiedName
      45. +
      46. GlobalConstantMask
      47. HexDigit
      48. +
      49. HexDigits
      50. HexEscape
      51. HexIndicator
      52. HexIntLiteral
      53. @@ -1815,6 +1951,7 @@

      54. IntersectionType
      55. IntLiteral
      56. LeadingFloatLiteral
      57. +
      58. LeadingMask
      59. Letter
      60. LineComment
      61. LineCommentStart
      62. @@ -1822,19 +1959,22 @@

      63. ListSuffix
      64. LiteralType
      65. LogicalType
      66. +
      67. LowerBound
      68. +
      69. MaskTail
      70. Name
      71. NameContinue
      72. NamedType
      73. NameStart
      74. NameToken
      75. NegativeSign
      76. +
      77. NonZeroDigit
      78. NullableType
      79. NullLiteral
      80. OctalDigit
      81. +
      82. OctalDigits
      83. OctalIndicator
      84. OctalIntLiteral
      85. OffsetSuffix
      86. -
      87. ParameterModifiers
      88. PostfixType
      89. PrimaryType
      90. Punctuator
      91. @@ -1846,6 +1986,7 @@

      92. ShapeFields
      93. ShapeKey
      94. ShapeValue
      95. +
      96. Sign
      97. SimpleEscape
      98. SingleQuotedString
      99. SingleStringChar
      100. @@ -1854,7 +1995,11 @@

      101. TemplateArgument
      102. TemplateArgumentHint
      103. TemplateArguments
      104. -
      105. TemplateArgumentType
      106. +
      107. TemplateArgumentValue
      108. +
      109. TemplateBound
      110. +
      111. TemplateDefault
      112. +
      113. TemplateParameter
      114. +
      115. TemplateParameters
      116. ThisVariable
      117. Token
      118. TrailingFloatLiteral
      119. @@ -1864,8 +2009,12 @@

      120. UnicodeEscape
      121. UnionType
      122. UnsealedShape
      123. +
      124. UpperBound
      125. +
      126. UpperBoundOperator
      127. Variable
      128. +
      129. VariableContinue
      130. Whitespace
      131. +
      132. Wildcard
      @@ -1947,7 +2096,6 @@

      1. 5.8.1Template Argument Hints
      2. -
      3. 5.8.2Template Argument Attributes
    1. 5.9Literal Types
    2. @@ -1970,17 +2118,15 @@

    3. 6.1.1Shape Fields
    4. 6.1.2Optional Fields
    5. 6.1.3Unsealed Shapes
    6. -
    7. 6.1.4Shape Field Attributes
  • 6.2Callable Types
      -
    1. 6.2.1Callable Parameters
    2. -
    3. 6.2.2Callable Parameter Attributes
    4. +
    5. 6.2.1Template Parameters
    6. +
    7. 6.2.2Callable Parameters
  • -
  • 6.3Attributes
  • AAppendix: Notation Conventions diff --git a/docs/Writerside/tl.tree b/docs/Writerside/tl.tree index e6d987a..a68d237 100644 --- a/docs/Writerside/tl.tree +++ b/docs/Writerside/tl.tree @@ -29,6 +29,7 @@ topic="comparison.md"> + diff --git a/docs/Writerside/topics/language/basic-types.md b/docs/Writerside/topics/language/basic-types.md index d59c4ad..825f436 100644 --- a/docs/Writerside/topics/language/basic-types.md +++ b/docs/Writerside/topics/language/basic-types.md @@ -115,3 +115,96 @@ The namespace delimiter can be used in conjunction with keywords such as `true`, + +## The $this Type {id="this-type"} + +The `$this` variable denotes the type of the object the type is written on. It +is a type of its own, so it stands wherever a type stands: inside a union, +behind a question mark, or as the type a +[callable](callable-types.md) returns. + +> `$this` is the one variable that is a type. Any other variable names a +> [callable parameter](callable-types.md#named-parameters) or an operand of a +> [conditional type](conditional-types.md), and is not a type on its own. +> {style="note"} + + + + +> The type of the current object. +> ```typescript +> $this +> ``` + +> The current object type, or nothing at all. +> ```typescript +> ?$this +> ``` + +> A callable returning the object it was called on. +> ```typescript +> callable(): $this +> ``` + + + + +> Any other variable is not a type. +> ```typescript +> $var +> ``` +> +> An error similar to the one below should occur +> ``` +> ParseException: Syntax error, unexpected end of input, T_EQ expected +> ``` +> {style="warning"} + + + + +## Comments + +A type MAY carry comments, and they are read the way whitespace is: they part +tokens and mean nothing of their own. + +A **line comment** begins with either a `//` or a `#` marker and runs up to the +end of the line. A **block comment** begins with a `/*` and runs up to and +including the next `*/`. + + + + +> Block comment standing between two tokens. +> ```typescript +> int /* comment */ | string +> ``` + +> Line comment ending a line of a type written across several. +> ```typescript +> int | // comment +> string +> ``` + +> A `#` marker begins a line comment as well. +> ```typescript +> # comment +> int +> ``` + + + + +> A comment is not a type, so a source made of one alone describes nothing. +> ```typescript +> // comment +> ``` +> +> An error similar to the one below should occur +> ``` +> ParseException: Syntax error, unexpected end of input +> ``` +> {style="warning"} + + + diff --git a/docs/Writerside/topics/language/callable-types.md b/docs/Writerside/topics/language/callable-types.md index 092f87d..7945d5e 100644 --- a/docs/Writerside/topics/language/callable-types.md +++ b/docs/Writerside/topics/language/callable-types.md @@ -148,8 +148,8 @@ parameter description. ### Variadic Parameters -Variadic parameters are indicated by the "`...`" and can be placed either -_before the type_ or _before the parameter name._ +Variadic parameters are indicated by the "`...`" placed after the type. Where +a parameter carries both markers, the ampersand ("`&`") comes first. > Variadic parameter cannot be optional since they are already optional. {style="warning"} @@ -157,24 +157,6 @@ _before the type_ or _before the parameter name._ -> Callable type with one variadic parameter. -> ```typescript -> foo(...T) -> ``` - -> Callable type with one variadic named parameter. -> ```typescript -> foo(...T $name) -> ``` - -> Callable type with one variadic output named parameter. -> ```typescript -> foo(...T &$name) -> ``` - - - - > Callable type with one variadic parameter. > ```typescript > foo(T...) @@ -193,9 +175,9 @@ _before the type_ or _before the parameter name._ -> The ellipses (`...`) must come before or after the type. +> The ellipsis (`...`) must be placed after the parameter's type. > ```typescript -> foo(...T...) +> foo(...T) > ``` > > An error similar to the one below should occur @@ -204,6 +186,17 @@ _before the type_ or _before the parameter name._ > ``` > {style="warning"} +> The ampersand (`&`) must be placed before the ellipsis (`...`). +> ```typescript +> foo(T ...&$name) +> ``` +> +> An error similar to the one below should occur +> ``` +> ParseException: Syntax error, unexpected "&" +> ``` +> {style="warning"} + > Variadic parameter cannot be optional. > ```typescript > foo(T ...$name=) @@ -218,47 +211,100 @@ _before the type_ or _before the parameter name._ +## Template Parameters + +A callable MAY declare the template parameters it introduces, written as a +`<...>` list between the name and the parameter list. Each parameter is a +name, optionally followed by the bounds put on it. + +A parameter accepts three kinds of limit, each written at most once. The two +bounds are written in either order, and the default is written last: a bound +behind it would read as a bound of the default itself. -## Attributes +* `of T` or `as T` — the **upper bound**: the argument is to be a subtype + of `T`. The two words mean the same and are kept as they are written. +* `super T` — the **lower bound**: the argument is to be a supertype of `T`. +* `= T` — the **default**: the type the parameter takes when no argument is + passed. It bounds nothing. - +The words are not case-sensitive, so an `OF` reads the same way an `of` does. -Each callable parameter allows you to define list of additional attributes. -An attribute is additional metadata for a parameter. +> A `<...>` that no parenthesis follows is a list of +> [template arguments](generic-types.md), not of template parameters, and +> template arguments describe no bounds. +> {style="note"} -> Simple attribute with one argument for each callable parameter. +> Callable type declaring one template parameter. > ```typescript -> Example\Functor(#[type(int8)] int $a): void +> callable(T): T > ``` -> Multiple attributes in one group. +> Callable type declaring a bounded template parameter. > ```typescript -> Example\Functor(#[type(int8), const] int&): void +> callable(T): T > ``` -> Multiple attribute groups. +> Every kind of bound, and several parameters at once. > ```typescript -> Example\OnCreate( -> #[deprecated] -> #[inline] -> (callback(T): void) $callback=, -> ): void +> Closure(T, U): V +> ``` + +> One parameter carrying every limit at once. +> ```typescript +> callable(T): void > ``` -> Only valid identifiers are allowed. +> Bounds belong to a callable alone, so a type used with template arguments +> describes none. +> ```typescript +> Collection +> ``` +> +> An error similar to the one below should occur +> ``` +> ParseException: Syntax error, unexpected end of input +> ``` +> {style="warning"} + +> Only `of`, `as` and `super` bound a parameter. +> ```typescript +> callable(): void +> ``` +> +> An error similar to the one below should occur +> ``` +> ParseException: Template parameter cannot be bounded with "whatever", +> expected one of "of", "as" or "super" +> ``` +> {style="warning"} + +> Each kind of limit is written at most once. +> ```typescript +> callable(): void +> ``` +> +> An error similar to the one below should occur +> ``` +> ParseException: Template parameter cannot have more than one upper bound +> ``` +> {style="warning"} + +> A bound cannot stand behind the default, since it would read as a bound +> of the default itself: the `of Some` below bounds the `int`, not the `T`. > ```typescript -> Example\Functor(#[42] int $a): void +> callable(): void > ``` > > An error similar to the one below should occur > ``` -> ParseException: Syntax error, unexpected "42" +> ParseException: Template parameter default must be written last, since +> a bound behind it reads as a bound of the default itself > ``` > {style="warning"} diff --git a/docs/Writerside/topics/language/comparison.md b/docs/Writerside/topics/language/comparison.md index d0840f7..c72ae0c 100644 --- a/docs/Writerside/topics/language/comparison.md +++ b/docs/Writerside/topics/language/comparison.md @@ -7,21 +7,8 @@ static code analysis tools: [PHPStan](https://phpstan.org/) and [Psalm](https:// - PHPStan: [https://phpstan.org](https://phpstan.org/writing-php-code/phpdoc-types) - Psalm: [https://psalm.dev](https://psalm.dev/docs/annotating_code/type_syntax/atomic_types/) - - -There are others that are not currently tested for compatibility. -For example: - -
  • PHAN: https://github.com/phan
  • -
  • phpDocumentor: https://docs.phpdoc.org
  • - - -If you wish, you can add information about this software using a - - pull request to the documentation -. - - +- Phan: [https://github.com/phan](https://github.com/phan/phan/wiki/About-Union-Types) +- phpDocumentor: [https://docs.phpdoc.org](https://docs.phpdoc.org/guide/guides/types.html) > Below is a comparison list of all syntactic structures (grammar). > The logical component (the physical existence of the type) is **NOT** @@ -40,26 +27,103 @@ General table across all type parsing capabilities  PHPStan + +  Phan + + +  phpDocumentor + - - 69/69 - + + 82/83 + - 43/69 + 40/83 - 47/69 + 69/83 + + + + + 31/83 + + + + + 62/83 +## Methodology + +Each verdict below is the answer of the tool's own parser, asked directly, so +the table says what the tools do rather than what they are said to do. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ToolVersionA type is supported when
    +  TypeLang + type-lang/parser 2.xTypeParser::parse() returns a node
    +  Psalm + vimeo/psalm 6.5Psalm\Type::parseString() returns a type carrying what was written
    +  PHPStan + phpstan/phpdoc-parser 2.3TypeParser::parse() reads the source whole, leaving no trailing input
    +  Phan + phan/phan 5.5 + UnionType::fromStringInContext() returns something other than + a class name made of the whole source +
    +  phpDocumentor + phpdocumentor/type-resolver 2.0TypeResolver::resolve() returns a type, the source read whole
    + +> Two of these tools answer even where they did not understand the question, and +> the rules above are what tells the two apart. +> +> Phan reads whatever it cannot parse as a class name, so a `0b1010` comes back +> as a class called `\0b1010` rather than as an error. phpDocumentor builds on +> `phpstan/phpdoc-parser` and keeps what it managed to read, so a `JSON_*` comes +> back as the `JSON_` in front of it, the mask silently dropped. A verdict here +> counts neither. +> {style="note"} + ## Basic Types Below is a list of simple, logical and other common types. @@ -76,27 +140,43 @@ Below is a list of simple, logical and other common types.  PHPStan + +  Phan + + +  phpDocumentor + - 12/12 + 20/20 - 9/12 + 10/20 - 10/12 + 14/20 + + + + + 8/20 + + + + + 14/20 - + Class or type name (including FQN) @@ -110,9 +190,11 @@ Below is a list of simple, logical and other common types. + + - + Logical union types @@ -126,14 +208,16 @@ Below is a list of simple, logical and other common types. + + - + Logical intersection types - T & U & V + T & U & V @@ -142,9 +226,11 @@ Below is a list of simple, logical and other common types. + + - + Logical nullable types @@ -158,14 +244,34 @@ Below is a list of simple, logical and other common types. + + - + + + Grouping parentheses + + + (T | U)[] + + + + + + + + + + + + + Legacy list types syntax - User[] + User[] @@ -174,33 +280,51 @@ Below is a list of simple, logical and other common types. + + - - - Template arguments (Generics) + + + Type offsets (offset access) - ExampleCollection<array-key, User> + ExampleShape['key'] + ClassName::CONSTANT[0] - - - - Trailing comma not supported 1 + + + + + + + + + Template arguments (Generics) + + ExampleCollection<array-key, User> + + + + + + Trailing comma not supported 1 + + - + - + ExampleCollection<array-key, User,> @@ -210,12 +334,12 @@ Below is a list of simple, logical and other common types. - + Template argument hints - ExampleCollection<in array-key, out User> + ExampleCollection<in array-key, out User> @@ -228,18 +352,20 @@ Below is a list of simple, logical and other common types. Not Supported - - - - Call-site variance 1 - - + Other keywords 1 + + Other keywords 1 - + - + +

    + The in and out hints are not read, but the + same thing is said with contravariant and + covariant. +

    Collection<covariant Animal> @@ -249,34 +375,70 @@ Below is a list of simple, logical and other common types. - - - Template argument attributes + + + Wildcard template arguments - ExampleCollection<#[assert(not<"0">)] array-key> + ExampleCollection<*> - - - Not Supported + + Read as a bivariant mixed 1 + + Read as a bivariant mixed 1 + + + + + + +

    + An asterisk is read as a mixed carrying a + bivariant hint, so the argument parses, but nothing + of the wildcard itself is left in the tree. +

    + + // Written + ExampleCollection<*> + + + // Read + ExampleCollection<bivariant mixed> + +
    +
    - - - Not Supported + + + + + The $this type + + + $this + - + + + + + + + + + Class constant types - ClassName::CONSTANT_NAME + ClassName::CONSTANT_NAME @@ -285,14 +447,16 @@ Below is a list of simple, logical and other common types. + + - + Prefixed class constant mask types - ClassName::CONSTANT_* + ClassName::CONSTANT_* @@ -301,30 +465,70 @@ Below is a list of simple, logical and other common types. + + - + Non-prefixed class constant mask types - ClassName::* + ClassName::* + + + + + + + + + + + + + + + Suffixed class constant mask types + + + ClassName::*_SUFFIX + + + + + + + + + + + + + + + Class constant mask types carrying both ends + + + ClassName::PREFIX_*_SUFFIX + + - + Global constant mask types - JSON_* + JSON_* @@ -343,6 +547,63 @@ Below is a list of simple, logical and other common types. Not Supported + + + + + + + Suffixed global constant mask types + + + *_SUFFIX + + + + + + + + + + + + + + + Namespaced constant mask types + + + Path\To\JSON_* + Path\To\* + + + + + + + + + + + + + + + Comments inside a type + + + int /* comment */ | string + + + + + + + + + + @@ -362,28 +623,44 @@ Below is a list of conditional types.  PHPStan + +  Phan + + +  phpDocumentor + - 10/10 + 6/6 - 4/10 + 4/6 - 4/10 + 3/6 + + + + + 0/6 + + + + + 3/6 - - + + Conditional positive equality types @@ -394,12 +671,30 @@ Below is a list of conditional types. - - + Brackets required 1 + Brackets required 1 + + Brackets required 1 - - + + + + +

    + A condition is written inside brackets and is only read there, so + the bare form below is read as the type T alone. +

    + + (T is A ? B : C) + +
    +
    + + + + +
    Conditional negative equality types @@ -410,12 +705,30 @@ Below is a list of conditional types. - - + Brackets required 1 + Brackets required 1 + + Brackets required 1 + + + + + + +

    + A condition is written inside brackets and is only read there, so + the bare form below is read as the type T alone. +

    + + (T is A ? B : C) + +
    +
    + - - + + Conditional referenced types @@ -427,17 +740,33 @@ Below is a list of conditional types. - - + Brackets required 1 + Brackets required 1 + + Brackets required 1 - - - Conditional referenced types - - - in Yoda-style + + + + +

    + A condition is written inside brackets and is only read there, so + the bare form below is read as the type T alone. +

    + + (T is A ? B : C) + +
    +
    + + + + +
    + Conditional referenced types + in Yoda-style A is $var ? B : C A is not $var ? B : C @@ -459,10 +788,12 @@ Below is a list of conditional types. Not Supported + + - - + + Functions in conditional types @@ -474,14 +805,16 @@ Below is a list of conditional types. - List of supported functions 1 - List of supported functions 2 + List of supported functions 1 + No call syntax 2 + + No call syntax 2 - + - +
  • define()
  • array_map()
  • @@ -494,24 +827,26 @@ Below is a list of conditional types.
  • class_alias()
  • - - -
  • func_get_arg()
  • -
  • func_get_args()
  • -
  • func_num_args()
  • -
    + +

    + A parenthesis behind a name ends the type, whatever the name is, + so the functions PHPStan reads in a condition are read by rules + of its own rather than by the type language. +

    + + (func_num_args() is 1 ? A : B) + // Unexpected token "(", expected ')' at offset 14 +
    - - + + Functions in conditional types - - in Yoda-style - + in Yoda-style A is foo() ? B : C A is not foo() ? B : C @@ -533,94 +868,8 @@ Below is a list of conditional types. Not Supported - - - - - Less-than conditional types - - - T < A ? B : C - - - - - - - - - Not Supported - - - - Not Supported - - - - - - Greater-than conditional types - - - T > A ? B : C - - - - - - - - - Not Supported - - - - Not Supported - - - - - - Less-than-or-equal conditional types - - - T <= A ? B : C - - - - - - - - - Not Supported - - - - Not Supported - - - - - - Greater-than-or-equal conditional types - - - T >= A ? B : C - - - - - - - - - Not Supported - - - - Not Supported - + + @@ -640,27 +889,43 @@ Below is a list of literal types/lexemes.  PHPStan + +  Phan + + +  phpDocumentor + - 19/19 + 21/21 + + + + + 7/21 + + + + + 21/21 - 7/19 + 5/21 - 11/19 + 15/21 - + Boolean true and false literals @@ -675,9 +940,11 @@ Below is a list of literal types/lexemes. + + - + The null literals @@ -691,9 +958,11 @@ Below is a list of literal types/lexemes. + + - + Single-quoted string literals @@ -707,9 +976,11 @@ Below is a list of literal types/lexemes. + + - + Double-quoted string literals @@ -723,9 +994,11 @@ Below is a list of literal types/lexemes. + + - + Escape sequences in a double-quoted string literals @@ -743,15 +1016,12 @@ Below is a list of literal types/lexemes. Not Supported - - - - Not Supported - - + + + - + Hexadecimal sequences in a double-quoted string literals @@ -769,15 +1039,12 @@ Below is a list of literal types/lexemes. Not Supported - - - - Not Supported - - + + + - + Unicode sequences in a double-quoted string literals @@ -795,15 +1062,12 @@ Below is a list of literal types/lexemes. Not Supported - - - - Not Supported - - + + + - + Integer literals @@ -817,42 +1081,53 @@ Below is a list of literal types/lexemes. + + - + BigInteger (PHP_INT_MAX + 1 or PHP_INT_MIN - 1) literals - 42 + 9999999999999999999 - Works with restrictions 1 - Works with restrictions 2 - Works with restrictions 3 + Kept in full 1 + Works with restrictions 2 + Kept in full 3 + Works with restrictions 4 + Works with restrictions 5 - + - + +

    + The $decimal carries the value written out in base + 10, so a number too large for the platform's int is + still readable in full. The $value beside it is the + native one, and that is the only one a platform limit applies to. +

    // Input 9999999999999999999 // Stored - TypeLang\Parser\Node\Literal\IntLiteralNode { + TypeLang\Type\Literal\IntLiteralNode { +offset: 0 - +raw: "9999999999999999999" +value: 9223372036854775807 + +raw: "9999999999999999999" + +decimal: "9999999999999999999" }
    - + // Input 9999999999999999999 @@ -863,22 +1138,55 @@ Below is a list of literal types/lexemes. Open in psalm.dev - + +

    + The literal is kept as it was written, so nothing is lost while + the type is read. The number does not survive the analysis that + follows, though: what comes out of it is the nearest + int the platform carries. +

    // Input 9999999999999999999 // Stored - 9223372036854775807 + PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode { + value: "9999999999999999999" + } + + + // Analysed + \PHPStan\dumpType($value); + // Dumped type: 9223372036854775807 Open in phpstan.org
    + + + // Input + 9999999999999999999 + + + // Stored + 1.0E+19 + + + + + // Input + 9999999999999999999 + + + // Stored + 9223372036854775807 + +
    - + Integer literals in binary format @@ -896,15 +1204,33 @@ Below is a list of literal types/lexemes. Not Supported - - - - Not Supported - + + + Value read as 0 1 + + + + + + +

    + The number parses, but the value behind it is built with a plain + cast, so nothing of the base survives. +

    + + // Written + 0b10101010 + + + // Read + 0 + +
    +
    - + Integer literals in octal format @@ -922,15 +1248,33 @@ Below is a list of literal types/lexemes. Not Supported - - - - Not Supported - + + + Value read as 0 1 + + + + + + +

    + The number parses, but the value behind it is built with a plain + cast, so nothing of the base survives. +

    + + // Written + 0o42 + + + // Read + 0 + +
    +
    - + Integer literals in legacy octal format @@ -948,15 +1292,33 @@ Below is a list of literal types/lexemes. Not Supported - - - - Not Supported - + + + Value read as decimal 1 + + + + + + +

    + The number parses, but the value behind it is built with a plain + cast, so nothing of the base survives. +

    + + // Written + 042 + + + // Read + 42 + +
    +
    - + Integer literals in hexadecimal format @@ -974,15 +1336,69 @@ Below is a list of literal types/lexemes. Not Supported - - - - Not Supported + + + Value read as 0 1 + + + + + + +

    + The number parses, but the value behind it is built with a plain + cast, so nothing of the base survives. +

    + + // Written + 0xDEAD_BEEF + + + // Read + 0 + +
    +
    + + + + + + Underscore (_) separators in integer literals + + + 42_04 + + + + + + + + + + + + + + + Explicitly signed number literals + + +42 + - + + + + + + + + + Float literals @@ -996,9 +1412,11 @@ Below is a list of literal types/lexemes. + + - + Float literals without leading zero @@ -1017,9 +1435,11 @@ Below is a list of literal types/lexemes. + + - + Float literals without trailing zero @@ -1038,9 +1458,11 @@ Below is a list of literal types/lexemes. + + - + Float literals in scientific notation @@ -1059,14 +1481,16 @@ Below is a list of literal types/lexemes. + + - + - Float literals in scientific notation + Float literals in scientific notation carrying a signed exponent - 2e2 + -1.5e+3 @@ -1080,11 +1504,13 @@ Below is a list of literal types/lexemes. + + - - - Float hexadecimal literals in scientific notation + + + Hexadecimal integer literals carrying an e 0x42e2 @@ -1100,11 +1526,29 @@ Below is a list of literal types/lexemes. Not Supported - - - - Not Supported - + + + Value read as 0 1 + + + + + + +

    + The number parses, but the value behind it is built with a plain + cast, so nothing of the base survives. +

    + + // Written + 0x42e2 + + + // Read + 0 + +
    +
    @@ -1113,7 +1557,6 @@ Below is a list of literal types/lexemes. Below is a list of grammar of shaped types. - @@ -1126,32 +1569,48 @@ Below is a list of grammar of shaped types. + + + + - - + + + - - - - - - + + + - - - - - - + + + - - + + - - + + + - - - - - - + + + - - - - - - + + + - - - + + + - - - - - - + + + - - - + + + + + + + + @@ -1497,56 +1861,40 @@ Below is a list of grammar of shaped types. - - + + + + - - - + + + + + + + + @@ -1554,22 +1902,19 @@ Below is a list of grammar of shaped types. - + + + - @@ -1577,18 +1922,19 @@ Below is a list of grammar of shaped types. - - + + + + - @@ -1596,12 +1942,35 @@ Below is a list of grammar of shaped types. + + - + + + + +
     PHPStan +  Phan + +  phpDocumentor +
    - 13/13 + 15/15 - 10/13 + 9/15 + + + + 13/15 + + + + 5/15 - 10/13 + 13/15
    - + + Explicit shape types - object { + array { key: ValueType } @@ -1160,38 +1619,18 @@ Below is a list of grammar of shaped types.
    Works with restrictions 1
    - - - - // OK - object { - key: ValueType - } - - - // Error - Custom\ObjectType { - key: ValueType - } - - Open in psalm.dev - - -
    - + + Trailing comma in explicit shape types - object { + array { key: ValueType, } @@ -1200,38 +1639,18 @@ Below is a list of grammar of shaped types.
    Works with restrictions 1
    - - - - // OK - object { - key: ValueType, - } - - - // Error - Custom\ObjectType { - key: ValueType, - } - - Open in psalm.dev - - -
    - + + Implicit shape types - object { + array { ValueType } @@ -1240,38 +1659,36 @@ Below is a list of grammar of shaped types.
    Works with restrictions 1 Arrays and lists only 1 Arrays and lists only 1
    + - + // OK - object { - ValueType - } + array { ValueType } + list { ValueType } - // Error - Custom\ObjectType { - ValueType - } + // Syntax Error + object { ValueType } - Open in psalm.dev
    - + + Trailing comma in implicit shape types - object { + array { ValueType, } @@ -1287,14 +1704,16 @@ Below is a list of grammar of shaped types.
    - + + Optional keys in explicit shape types - object { + array { key?: ValueType } @@ -1303,106 +1722,75 @@ Below is a list of grammar of shaped types.
    Works with restrictions 1
    - - - - // OK - object { - key?: ValueType - } - - - // Error - Custom\ObjectType { - key?: ValueType - } - - Open in psalm.dev - - -
    + Empty (closed) shape types - object {} + array {}
    Works with restrictions 1
    - - - - // OK - object {} - - - // Error - Custom\ObjectType {} - - Open in psalm.dev - - -
    + Unsealed shape types - object { ... } + array { ... }
    Works with restrictions 1 Read as an empty array 1
    + - + +

    + A shape carrying fields keeps what follows them, but one made of + the ellipsis alone comes back sealed and empty. +

    - // OK - object { ... } + // Written + array { ... } - // Error - Custom\ObjectType { ... } + // Read + array<never, never> - Open in psalm.dev
    + Explicit unsealed shape types - object { + array { key: ValueType, ... } @@ -1412,40 +1800,18 @@ Below is a list of grammar of shaped types.
    Works with restrictions 1
    - - - - // OK - object { - key: ValueType, - ... - } - - - // Error - Custom\ObjectType { - key: ValueType, - ... - } - - Open in psalm.dev - - -
    + Implicit unsealed shape types - object { + array { ValueType, ... } @@ -1455,41 +1821,39 @@ Below is a list of grammar of shaped types.
    Works with restrictions 1
    - - - - // OK - object { - ValueType, - ... - } - - - // Error - Custom\ObjectType { - ValueType, - ... - } - - Open in psalm.dev - - + + + Typed unsealed shape types + + + array { + ...<array-key, ValueType> + } +
    - - Typed unsealed shape types +
    + + Class constant shape keys - object { - ...<array-key, Type> + array { + Path\To\ClassName::CONSTANT_NAME: string, }
    Works with restrictions 1 Works with restrictions 2
    - - - - // OK - object { - ...<array-key, Type> - } - - - // Error - Custom\ObjectType { - ...<array-key, Type> - } - - Open in psalm.dev - - - - // OK - array { - ...<array-key, Type> - } - - - // Syntax Error - list { - array-key, - ...<Type> - } - - Open in phpstan.org - - + + + Constant mask shape keys + + + array { + Path\To\ClassName::PREFIX_*: string, + JSON_*: string, + } +
    - - Shape field attributes +
    + + String literal shape keys - object { - #[inline, assert(not<"">)] - name: string + array { + 'some key': ValueType, }
    - - - PHPStan parser only (since 2.3) - -
    - - Class constant shape keys + + + Numeric shape keys - array{ - Path\To\ClassName::CONSTANT_NAME: string, + array { + 0: ValueType, }
    - - Constant mask shape keys + + + Shapes of an arbitrary type name - array{ - Path\To\ClassName::PREFIX_*: string, - JSON_*: string, + Custom\ObjectType { + key: ValueType }
    Fixed set of names 1 Fixed set of names 2 Fixed set of names 2
    + + +

    + A shape follows an array, a list, an + object or a callable-array, and no other + name. +

    + Open in psalm.dev +
    + +

    + A shape follows an array, a list or an + object, and no other name. +

    +
    +
    +
    -## Callables Types +## Callable Types Below is a list of grammar of callable (function) types. @@ -1617,27 +1986,43 @@ Below is a list of grammar of callable (function) types.  PHPStan + +  Phan + + +  phpDocumentor + - - 15/15 - + + 20/21 + - 13/15 + 10/21 + + + + + 18/21 + + + + + 13/21 - 12/15 + 17/21 - + Non-typed callable types @@ -1656,9 +2041,11 @@ Below is a list of grammar of callable (function) types. Not Supported + + - + Typed callable types @@ -1672,9 +2059,11 @@ Below is a list of grammar of callable (function) types. + + - + Callable with typed parameters @@ -1688,9 +2077,29 @@ Below is a list of grammar of callable (function) types. + + + + + + + Trailing comma in the parameter list + + + callable(Type,): T + + + + + + + + + + - + Callable with optional parameters @@ -1704,9 +2113,11 @@ Below is a list of grammar of callable (function) types. + + - + Callable with named parameters @@ -1720,10 +2131,12 @@ Below is a list of grammar of callable (function) types. + + - - + + Callable with optional named parameters @@ -1741,36 +2154,40 @@ Below is a list of grammar of callable (function) types. + + - + Callable with output parameters - callable(T&): T + callable(T&): T - Works with restrictions 1 + Works with restrictions 1 + + - + - + // OK - callable(T&): U + callable(T&): U // Bug: Intersection types must be all objects, // Psalm\Type\Atomic\TInt provided in docblock - callable(int&): U + callable(int&): U Open in psalm.dev @@ -1778,47 +2195,49 @@ Below is a list of grammar of callable (function) types. - - + + Callable with output optional parameters - callable(T&=): T + callable(T&=): T - Works with restrictions 1 + Works with restrictions 1 + + - + - + // OK - callable(T&=): U + callable(T&): U // Bug: Intersection types must be all objects, // Psalm\Type\Atomic\TInt provided in docblock - callable(int&=): U + callable(int&): U - Open in psalm.dev + Open in psalm.dev - - + + Callable with output optional named parameters - callable(T &$name=): T + callable(T &$name=): T @@ -1832,9 +2251,11 @@ Below is a list of grammar of callable (function) types. + + - + Callable with suffixed variadic parameters @@ -1848,14 +2269,16 @@ Below is a list of grammar of callable (function) types. + + - + - Callable with prefixed variadic parameters + Callable with named variadic parameters - callable(...Type): T + callable(Type ...$name): T @@ -1863,90 +2286,112 @@ Below is a list of grammar of callable (function) types. - - - - Not Supported + + + + + + + + Callable with output named variadic parameters + + callable(Type &...$name): T + - + + + + + + + + + - Callable with prefixed and suffixed variadic parameters + Callable with prefixed variadic parameters - callable(...Type ...$name): T + callable(...Type): T + Suffix form only 1 - Works with restrictions 1 - Works with restrictions 2 + + + + Not Supported + + + Parameter dropped 2 + - - - This expression is incorrect because the "variadic" - lexeme ... must be present in the parameter - in a single copy. The parser must throw a syntax or - semantic error. - + - + +

    + The ellipsis stands behind the type, the way PHP itself writes it. + The prefix form was read by earlier versions of the parser and is + a syntax error now. +

    - callable(...int ...$name) - // Internal Psalm error on line ...: - // Unrecognised parse tree type Psalm\Internal\Type\ParseTree\CallableParamTree + callable(Type...): T - Open in psalm.dev
    - + - callable(...int ...$name) - // PHPDoc tag @return has invalid value (callable(...int ...$name): T): - // Unexpected token "(", expected TOKEN_HORIZONTAL_WS at ... + // Written + callable(...Type): T + + + // Read + callable(): T - Open in phpstan.org
    - + Callable with optional variadic parameters callable(Type...=): T - callable(...Type=): T + callable(Type ...$name=): T - Works with restrictions 1 + Error depends on the spelling 1 - + No Error + No Error + No Error - + - This expression is incorrect because any variadic - parameter is already optional. The parser must - throw a syntax or semantic error. + This expression is incorrect because any variadic parameter is + already optional. The parser must throw a syntax or semantic + error. - + // OK: Cannot have variadic param with a default in docblock callable(...T=): T @@ -1961,87 +2406,153 @@ Below is a list of grammar of callable (function) types. - + - Callable with named variadic parameters + Callable carrying the variadic marker twice - callable(Type ...$name): T - callable(...Type $name): T + callable(...Type ...$name): T - Works with restrictions 1 - Works with restrictions 2 + Internal error 1 + + No Error + No Error - + + + This expression is incorrect because the "variadic" lexeme + ... must be present in the parameter in a single + copy. The parser must throw a syntax or semantic error. + - + - callable(...Type $name): T - // Internal Psalm error on line ...: + callable(...int ...$name) + // Internal Psalm error on line ...: // Unrecognised parse tree type Psalm\Internal\Type\ParseTree\CallableParamTree - Open in psalm.dev - - - - callable(...Type $name): T - // PHPDoc tag @return has invalid value (callable(...Type $name): T): - // Unexpected token "(", expected TOKEN_HORIZONTAL_WS at ... - - Open in phpstan.org + Open in psalm.dev - - - Callable with output named variadic parameters + + + The $this return type + + + callable(): $this + + + + + + + + + + + + + + + Callable declaring template parameters + + + callable<T>(T): T + + + + + + + + + + + + + + + Upper bound of a template parameter - callable(Type &...$name): T - callable(...Type &$name): T + callable<T of Some>(T): T + callable<T as Some>(T): T - Works with restrictions 1 - Works with restrictions 2 + + Both words read as one 1 + + Both words read as one 1 - + - + +

    + An as comes back as an of, where TypeLang + keeps the bound as it was written. +

    - // OK - callable(T &...$name): U + // Written + callable<T as Some>(T): T - // Bug: Intersection types must be all objects, - // Psalm\Type\Atomic\TInt provided in docblock - callable(int &...$name): U - - Open in psalm.dev -
    - - - callable(...Type &$name): T - // PHPDoc tag @return has invalid value (callable(...Type &$name): T): - // Unexpected token "(", expected TOKEN_HORIZONTAL_WS at ... + // Read + callable<T of Some>(T): T - Open in phpstan.org
    + + + + Lower bound of a template parameter + + + callable<T super Some>(T): T + + + + + + + + + + + + + + + Default of a template parameter + + + callable<T = int>(T): T + + + + + + + + + + + diff --git a/docs/Writerside/topics/language/conditional-types.md b/docs/Writerside/topics/language/conditional-types.md index 409005a..b3854d9 100644 --- a/docs/Writerside/topics/language/conditional-types.md +++ b/docs/Writerside/topics/language/conditional-types.md @@ -101,42 +101,6 @@ The `is` keyword checks whether the subject **is** of the compared type, and -## Comparison Operators - -In addition to the `is` / `is not` equality checks, TypeLang also allows -comparing the subject using the `<`, `>`, `<=` and `>=` operators. - - - - -> Less than. -> ```typescript -> T < int<0, 100> ? A : B -> ``` - -> Greater than. -> ```typescript -> T > 0 ? A : B -> ``` - -> Less than or equal. -> ```typescript -> T <= 100 ? A : B -> ``` - -> Greater than or equal. -> ```typescript -> T >= 0 ? A : B -> ``` - - - - -> This is a TypeLang-specific extension: neither PHPStan nor Psalm support -> comparison operators in conditional types, only `is` and `is not`. See the -> [syntax comparison](comparison.md) page for details. -> {style="note"} - ## Yoda-Style Conditions Since the subject and the compared type can both be arbitrary type diff --git a/docs/Writerside/topics/language/const-types.md b/docs/Writerside/topics/language/const-types.md index 5ede64b..4867836 100644 --- a/docs/Writerside/topics/language/const-types.md +++ b/docs/Writerside/topics/language/const-types.md @@ -85,26 +85,59 @@ a double colon (`::`) character, and then the constant name. ## Constant Masks -A reference to a certain set of constants can be defined using a mask. The -use of masks is identical to regular constants, but must be terminated with -an asterisk (`*`). +A reference to a certain set of constants can be defined using a mask: A +sequence of name segments with an asterisk (`*`) standing wherever the name +is left unsaid. + +A mask is made of at least one asterisk, and its segments and asterisks +alternate, so two asterisks in a row describe nothing a single one does not. Prefixes on class constants can be omitted, so type will mean any class constant. +A global constant may be left unsaid from its very beginning as well, and the +namespace it belongs to may be written in front of the mask. A name separated +from the mask by a backslash is a namespace whole, so the mask then means any +constant that namespace holds. -> Reference to any constant with the `JSON_*` prefix. +> Reference to any constant with the `JSON_` prefix. > ```typescript > JSON_* > ``` +> Reference to any constant with the `_SUFFIX` suffix. +> ```typescript +> *_SUFFIX +> ``` + +> Reference to any constant with the `JSON_` prefix in the `Path\To` namespace. +> ```typescript +> Path\To\JSON_* +> ``` + +> Reference to any constant of the `Path\To` namespace. +> ```typescript +> Path\To\* +> ``` + > Reference to any class constant with the `PREFIX_` prefix. > ```typescript > Path\To\ClassName::PREFIX_* > ``` +> Reference to any class constant with the `_SUFFIX` suffix. +> ```typescript +> Path\To\ClassName::*_SUFFIX +> ``` + +> Reference to any class constant written of the `PREFIX_` and `_SUFFIX` +> segments, in that order. +> ```typescript +> Path\To\ClassName::PREFIX_*_SUFFIX +> ``` + > Reference to any class constant > ```typescript > Path\To\ClassName::* @@ -113,25 +146,27 @@ Prefixes on class constants can be omitted, so type will mean any class constant -> It is not allowed to omit prefixes from global constants. +> A global mask needs at least one segment of a name, whether it stands in +> front of the asterisk or behind it. A lone asterisk would mean any constant +> there is, and that is not a type. > ```typescript > * > ``` > > An error similar to the one below should occur > ``` -> ParseException: Syntax error, unexpected "*" +> ParseException: Syntax error, unexpected end of input > ``` > {style="warning"} -> The asterisk (`*`) must be the final character. +> Two asterisks in a row are not a mask. > ```typescript -> Path\To\ClassName::PREFIX_*_SUFFIX +> Path\To\ClassName::PREFIX_** > ``` > > An error similar to the one below should occur > ``` -> ParseException: Syntax error, unexpected "_SUFFIX" +> ParseException: Syntax error, unexpected "*" > ``` > {style="warning"} diff --git a/docs/Writerside/topics/language/generic-types.md b/docs/Writerside/topics/language/generic-types.md index 8905c84..2685cb2 100644 --- a/docs/Writerside/topics/language/generic-types.md +++ b/docs/Writerside/topics/language/generic-types.md @@ -114,7 +114,7 @@ used, for example, in static analyzers to indicate the > > An error similar to the one below should occur > ``` -> ParseException: Syntax error, unexpected "Request" +> ParseException: Syntax error, unexpected end of input > ``` > {style="warning"} @@ -122,35 +122,41 @@ used, for example, in static analyzers to indicate the -## List Syntax +## Wildcard Arguments -In addition to modern list declarations such as `list` or -`array`, the legacy `int[]` syntax is allowed. +A template argument may be left unsaid, written as an asterisk (`*`) in the +place of the type. A wildcard argument accepts a [hint](#call-site-hints) +the same way any other argument does. -> List (array) of `User` objects. +> Any argument at all. > ```typescript -> User[] +> Collection<*> > ``` -> List of list (nested array) of `User` objects. +> A wildcard beside a type. > ```typescript -> User[][] +> HashMap +> ``` + +> A wildcard carrying a hint. +> ```typescript +> Collection > ``` -> Incorrect syntax (was [used in the PSR](https://github.com/php-fig/event-dispatcher/blob/1.0.0/src/ListenerProviderInterface.php#L14) by mistake). +> An asterisk is a template argument alone and is not a type of its own. > ```typescript -> User[int] +> int|* > ``` > > An error similar to the one below should occur > ``` -> ParseException: Syntax error, unexpected "int" +> ParseException: Syntax error, unexpected "*" > ``` > {style="warning"} @@ -158,42 +164,35 @@ In addition to modern list declarations such as `list` or -## Attributes - - +## List Syntax -Each template argument allows you to define list of additional attributes. -An attribute is additional metadata for an argument. +In addition to modern list declarations such as `list` or +`array`, the legacy `int[]` syntax is allowed. -> Simple attribute with one argument for each template argument. -> ```typescript -> HashMap<#[name("key")] T, #[name("value")] U> -> ``` - -> Multiple attributes in one group. +> List (array) of `User` objects. > ```typescript -> HashMap<#[name("key"), out] T> +> User[] > ``` -> Multiple attribute groups. +> List of list (nested array) of `User` objects. > ```typescript -> HashMap<#[name("key")] #[out] T> +> User[][] > ``` -> Only valid identifiers are allowed. +> Incorrect syntax (was [used in the PSR](https://github.com/php-fig/event-dispatcher/blob/1.0.0/src/ListenerProviderInterface.php#L14) by mistake). > ```typescript -> Collection<#[42] User> +> User[int] > ``` > > An error similar to the one below should occur > ``` -> ParseException: Syntax error, unexpected "42" +> ParseException: Syntax error, unexpected "int" > ``` > {style="warning"} diff --git a/docs/Writerside/topics/language/literal-types.md b/docs/Writerside/topics/language/literal-types.md index 4a7bdaf..6b8baaa 100644 --- a/docs/Writerside/topics/language/literal-types.md +++ b/docs/Writerside/topics/language/literal-types.md @@ -125,13 +125,58 @@ representation. The braces are required in the sequence. E.g. `"\u{41}" === "A"` The syntax of integer numbers is similar to the PHP language. Binary, octal, decimal and hexadecimal number systems are supported. -> Please note that in addition to numbers, underscores (`_`) are allowed. +> Please note that in addition to numbers, underscores (`_`) are allowed as a +> separator. A separator stands between two digits and nowhere else, so it +> may neither lead a number, nor trail it, nor stand beside another separator. > {style="note"} Decimal numbers can contain any digits from `0` to `9` (leading `0` not allowed) and must match the regular expression `[1-9][0-9]*`. For any other numbers, -a special prefixed format is used, described below. Negative values are prefixed -with a minus (`-`). +a special prefixed format is used, described below. A value may be prefixed by +a sign: a minus (`-`) makes it negative, and a plus (`+`) says what the absence +of a sign already says. + +> A number is kept whole, however large it is. The node carries both the +> native `$value`, which a platform limit applies to, and the `$decimal` +> beside it — the value written out in base 10 as a string, so a number too +> large for an `int` is still readable in full. +> {style="note"} + + + + +> A long number written with separators. +> ```typescript +> 42_04 +> ``` + + + + +> A separator cannot trail a number. +> ```typescript +> 42_ +> ``` +> +> An error similar to the one below should occur +> ``` +> ParseException: Syntax error, unexpected "_" +> ``` +> {style="warning"} + +> Two separators in a row are not allowed. +> ```typescript +> 4__2 +> ``` +> +> An error similar to the one below should occur +> ``` +> ParseException: Syntax error, unexpected "__2" +> ``` +> {style="warning"} + + + ### Binary @@ -212,6 +257,11 @@ numbers between `0` and `7` and must match the regular expression > 0o42_23 > ``` +> Number `275` in "legacy" octal format with `_` delimiters. +> ```typescript +> 04_23 +> ``` + @@ -226,6 +276,18 @@ numbers between `0` and `7` and must match the regular expression > ``` > {style="warning"} +> A leading `0` makes a number octal, so a digit outside the octal range +> cannot follow one. +> ```typescript +> 08 +> ``` +> +> An error similar to the one below should occur +> ``` +> ParseException: Syntax error, unexpected "8" +> ``` +> {style="warning"} + @@ -273,7 +335,18 @@ The syntax of float numbers is similar to the PHP language. Basic floating point syntax and scientific notation are supported. Every floating point number accepts the format `[0-9]+\.[0-9]+`. The leading or -trailing number may be omitted. Negative values are prefixed with a minus (`-`. +trailing number may be omitted. A value may be prefixed by a sign: a minus (`-`) +makes it negative, and a plus (`+`) says what the absence of a sign already says. + +> A number is kept whole, however large it is. The node carries both the +> native `$value`, which a platform limit applies to, and the `$decimal` +> beside it — the value written out in base 10 as a string, so a number too +> large for an `int` is still readable in full. +> {style="note"} + +Every run of digits a float is written of — the one before the dot, the one +after it and the exponent alike — accepts the underscore (`_`) separator under +the same rule an integer does. @@ -298,6 +371,17 @@ trailing number may be omitted. Negative values are prefixed with a minus (`-`. > -0.9 > ``` +> Signed floating point literal value (equivalent of `0.9`). +> ```typescript +> +0.9 +> ``` + +> Floating point literal value written with separators in every part +> (equivalent of `23.45e-67`). +> ```typescript +> 2_3.4_5e-6_7 +> ``` + @@ -352,6 +436,18 @@ writing out an inconveniently long string of digits. > 10e-2 > ``` +> Also number `1000.0` in scientific notation: The exponent takes a sign of +> its own, and a plus says what the absence of a sign already says. + +> A number is kept whole, however large it is. The node carries both the +> native `$value`, which a platform limit applies to, and the `$decimal` +> beside it — the value written out in base 10 as a string, so a number too +> large for an `int` is still readable in full. +> {style="note"} +> ```typescript +> 10e+2 +> ``` + diff --git a/docs/Writerside/topics/language/shape-types.md b/docs/Writerside/topics/language/shape-types.md index 772f062..15d2f25 100644 --- a/docs/Writerside/topics/language/shape-types.md +++ b/docs/Writerside/topics/language/shape-types.md @@ -196,62 +196,3 @@ array{ } ``` - - -## Attributes - - - -Each shape field allows you to define list of additional attributes. -An attribute is additional metadata for a field. - - - - -> Simple attribute with one argument for each shape field. -> ```typescript -> App\Domain\User{ -> #[name("user_name")] -> userName: non-empty-string, -> #[skip_when_empty] -> friends: list, -> ... -> } -> ``` - -> Multiple attributes in one group. -> ```typescript -> App\Domain\User{ -> #[complexity(100), skip_when_empty] -> friends: list, -> } -> ``` - -> Multiple attribute groups. -> ```typescript -> array{ -> #[serialize("onSerialize")] -> #[deserialize("onDeserialize")] -> test?: App\Domain\User, -> } -> ``` - - - - -> Only valid identifiers are allowed. -> ```typescript -> Collection{ -> #[42] -> test?: App\Domain\User, -> } -> ``` -> -> An error similar to the one below should occur -> ``` -> ParseException: Syntax error, unexpected "42" -> ``` -> {style="warning"} - - - diff --git a/docs/Writerside/topics/parser.md b/docs/Writerside/topics/parser.md index 49fcfed..e585432 100644 --- a/docs/Writerside/topics/parser.md +++ b/docs/Writerside/topics/parser.md @@ -2,8 +2,8 @@ -Parses TypeLang syntax into an AST of `TypeLang\Type\*` nodes, with strict and -tolerant parsing modes, feature toggling, and grammar checking. +Parses TypeLang syntax into an AST of `TypeLang\Type\*` nodes, with strict, +partial and validating reading modes, feature toggling, and grammar checking. @@ -21,7 +21,7 @@ out of it, checking the grammar along the way. **Requirements:** -* `PHP >= 8.4` +* `PHP >= 8.1` ## Quick Start @@ -40,23 +40,25 @@ var_dump($type); ```php object(TypeLang\Type\NamedTypeNode)#1 (4) { ["offset"]=> int(0) - ["name"]=> object(TypeLang\Type\Name)#2 (3) { + ["name"]=> object(TypeLang\Type\Name)#2 (5) { ["offset"]=> int(0) - ["segments"]=> array(1) { + ["parts"]=> array(1) { [0]=> object(TypeLang\Type\Identifier)#3 (2) { ["offset"]=> int(0) ["value"]=> string(5) "array" } } + ["first"]=> object(TypeLang\Type\Identifier)#3 { ... } + ["last"]=> object(TypeLang\Type\Identifier)#3 { ... } ["isFullyQualified"]=> bool(false) } ["arguments"]=> NULL ["fields"]=> object(TypeLang\Type\Shape\FieldsListNode)#4 (3) { ["offset"]=> int(7) ["items"]=> array(1) { - [0]=> object(TypeLang\Type\Shape\NamedFieldNode)#5 (5) { ... } + [0]=> object(TypeLang\Type\Shape\NamedFieldNode)#5 (4) { ... } } - ["sealed"]=> bool(true) + ["isSealed"]=> bool(true) } } ``` @@ -68,30 +70,32 @@ node classes themselves (`TypeLang\Type\*`) belong to the separate `type-lang/types` package — plain AST Nodes. > If a statement cannot be parsed, `parse()` throws an exception implementing -> `TypeLang\Parser\Exception\ParserExceptionInterface`. See the [tolerant -> mode](tolerant-mode.md) page for a way to parse partially valid input +> `TypeLang\Parser\Exception\ParserExceptionInterface`. See the [partial +> parsing](tolerant-mode.md) page for a way to read partially valid input > instead of failing outright. > {style="note"} -## Strict vs. Tolerant Parsing +## Three Ways of Reading a Source -`TypeParser` implements two parsing strategies, both declared on -`TypeParserInterface`: +`TypeParser` asks the grammar three different questions, all three declared +on `TypeParserInterface`: * `parse(): TypeNode` — strict mode. Requires the whole input to be a syntactically valid type statement; throws a `ParserExceptionInterface` on the first error. -* `parseTolerant(): ParsedResult` — tolerant mode. Parses as much of the - input as it can and returns a `TypeLang\Parser\ParsedResult` object - containing the (possibly partial) type and the offset up to which the - source was actually consumed — regardless of what follows. Useful for - phpdoc/docblock parsing where a type declaration is followed by a - free-text description. See [Tolerant mode](tolerant-mode.md). +* `partial(): ParsedResult` — reads as much of the input as the grammar + describes and returns a `TypeLang\Parser\Partial\ParsedResult` carrying the + type built out of the read part and the offset the reading stopped at, + whatever follows it. Useful for phpdoc/docblock parsing, where a type + declaration is followed by a free-text description. See + [](tolerant-mode.md). +* `validate(): CheckResult` — tells whether the source is a type, building + nothing of it. This is the cheapest of the three whenever the type itself + is of no use. See [](tolerant-mode.md#validation). ## Parser Arguments -The `parse()`/`parseTolerant()` methods accept the source code in -any of the following forms: +All three methods accept the source code in any of the following forms: @@ -137,10 +141,11 @@ Enable or disable individual language constructs (generics, shapes, unions, ...) — see [](features.md).
    - + -Parse a type declaration embedded in free-form text, such as a phpdoc -annotation — see [](tolerant-mode.md). +Read a type declaration embedded in free-form text, such as a phpdoc +annotation, or ask whether a source is a type at all — see +[](tolerant-mode.md). diff --git a/docs/Writerside/topics/parser/features.md b/docs/Writerside/topics/parser/features.md index 96c2510..53533f3 100644 --- a/docs/Writerside/topics/parser/features.md +++ b/docs/Writerside/topics/parser/features.md @@ -31,7 +31,7 @@ $parser->parse('42'); ``` TypeLang\Parser\Exception\SemanticParseException: -Literal values not allowed in "42" at column 1 +Literal values not allowed in "42" ``` To override one or more flags on an *existing* parser, use `TypeParser::withFeatures()` @@ -59,7 +59,7 @@ $features = new TypeParserFeatures() | Flag | Enables syntax | Error message fragment | |------------------|------------------------------------|------------------------------------------| | `literals` | `42`, `"foo"`, `true`, `null`, ... | `Literal values not allowed` | -| `generics` | `Example` | `Template arguments not allowed` | +| `generics` | `Example`, `callable(T): T` | `Template arguments not allowed` | | `hints` | `Example` | `Template argument hints not allowed` | | `lists` | `Example[]` | `Square bracket list types not allowed` | | `offsets` | `Example[Type]` | `Type offsets not allowed` | @@ -68,7 +68,6 @@ $features = new TypeParserFeatures() | `unions` | `T\|U` | `Union types not allowed` | | `intersections` | `T&U` | `Intersection types not allowed` | | `conditions` | `T is U ? A : B` | `Conditional expressions not allowed` | -| `attributes` | `#[attr]` on fields/args/params | see below | ### Literals @@ -84,7 +83,7 @@ $parser->parse('42'); ``` ``` -Literal values not allowed in "42" at column 1 +Literal values not allowed in "42" ``` ### Generics @@ -101,7 +100,19 @@ $parser->parse('Example'); ``` ``` -Template arguments not allowed in "Example" at column 8 +Template arguments not allowed in "Example" +``` + +The same flag closes the other side of the angle brackets: the +[template parameters](callable-types.md#template-parameters) a callable +declares. + +```php +$parser->parse('callable(T): T'); +``` + +``` +Template parameters not allowed in "callable(T): T" ``` ### Hints @@ -117,7 +128,7 @@ $parser->parse('Example'); ``` ``` -Template argument hints not allowed in "Example" at column 9 +Template argument hints not allowed in "Example" ``` ### Lists @@ -133,7 +144,7 @@ $parser->parse('Example[]'); ``` ``` -Square bracket list types not allowed in "Example[]" at column 1 +Square bracket list types not allowed in "Example[]" ``` ### Offsets @@ -151,7 +162,7 @@ $parser->parse('Example[Type]'); ``` ``` -Type offsets not allowed in "Example[Type]" at column 1 +Type offsets not allowed in "Example[Type]" ``` ### Callables @@ -167,7 +178,7 @@ $parser->parse('fn(): void'); ``` ``` -Callable types not allowed in "fn(): void" at column 1 +Callable types not allowed in "fn(): void" ``` ### Shapes @@ -185,7 +196,7 @@ $parser->parse(<<<'PHP' ``` ``` -Shape fields not allowed in "array{foo: T}" on line 1 at column 6 +Shape fields not allowed in "array{foo: T}" ``` ### Unions @@ -199,7 +210,7 @@ $parser->parse('T|U'); ``` ``` -Union types not allowed in "T|U" at column 1 +Union types not allowed in "T|U" ``` ### Intersections @@ -213,7 +224,7 @@ $parser->parse('T&U'); ``` ``` -Intersection types not allowed in "T&U" at column 1 +Intersection types not allowed in "T&U" ``` ### Conditions @@ -230,50 +241,5 @@ $parser->parse('T is U ? 23 : 42'); ``` ``` -Conditional expressions not allowed in "T is U ? 23 : 42" at column 1 -``` - -### Attributes - -Unlike the other flags, `attributes` is checked independently at every -place attributes may occur — currently on -[shape fields](shape-types.md#attributes), on -[template arguments](generic-types.md#attributes), and on -[callable parameters](callable-types.md#attributes) — each reporting its own -message. - -```php -$parser = new TypeParser(new TypeParserFeatures( - attributes: false, -)); - -$parser->parse(<<<'PHP' - array{ - #[name("new_name"), skip_when_empty] - oldName: int, - } - PHP); -``` - -``` -Shape field attributes not allowed in "array{\n #[name("new_name"), -…" (33+) on line 2 at column 5 -``` - -```php -$parser->parse('Collection<#[a] T>'); -``` - -``` -Template argument attributes not allowed in "Collection<#[a] T>" -at column 12 -``` - -```php -$parser->parse('callable(#[a] int $x): void'); -``` - -``` -Callable parameter attributes not allowed in "callable(#[a] int $x): void" -at column 10 +Conditional expressions not allowed in "T is U ? 23 : 42" ``` diff --git a/docs/Writerside/topics/parser/tolerant-mode.md b/docs/Writerside/topics/parser/tolerant-mode.md index b1e8fce..c8124c9 100644 --- a/docs/Writerside/topics/parser/tolerant-mode.md +++ b/docs/Writerside/topics/parser/tolerant-mode.md @@ -1,15 +1,15 @@ -# Tolerant Mode +# Partial Parsing -Parse as much of a type declaration as possible instead of failing on the -first syntax error, and recover the offset at which parsing actually stopped. +Read as much of a type declaration as the grammar describes instead of failing +on the first syntax error, recover the offset the reading stopped at, or ask +whether a source is a type at all. -The "tolerant" analysis mode allows parsing a type grammar embedded in other -arbitrary data, without requiring the full input to be a valid type -statement. +Partial parsing allows reading a type grammar embedded in other arbitrary +data, without requiring the full input to be a valid type statement. This is convenient, for example, for analyzing phpdoc (docblocks): a `@return` annotation contains a type declaration followed by a free-text @@ -17,20 +17,32 @@ description, and there is no delimiter between the two other than "the type grammar stops making sense here". Unlike `parse()`, which either returns a fully valid `TypeNode` or throws, -`TypeParser::parseTolerant()` always returns a `TypeLang\Parser\ParsedResult` -object: +`TypeParser::partial()` always returns a +`TypeLang\Parser\Partial\ParsedResult`, and which of the three it is says how +much of the source the grammar described. -```php -final readonly class ParsedResult -{ - public TypeNode $type; - - /** - * Last processed token offset, in bytes. - */ - public int $offset; -} -``` + + + +The source is a type whole, so the reading stopped at the very end of it. +Carries the `$type` built out of it and the `$offset` the reading stopped at. + + + + +The grammar described the beginning of the source alone. This is a successful +result as well, since a type has been built either way — the rest of the +source simply begins at the `$offset`. + + + + +The source opens no type at all, so nothing has been built of it. Carries the +`$message` of what stands in the way, the `$position` (line and column) and +the `$offset` the reading stopped at. + + + ## Basic Usage @@ -39,8 +51,9 @@ use TypeLang\Parser\TypeParser; $parser = new TypeParser(); -$result = $parser->parseTolerant('int and more text'); +$result = $parser->partial('int and more text'); +var_dump($result::class); // PartialParsedResult var_dump($result->type->name->toString()); // string(3) "int" var_dump($result->offset); // int(4) ``` @@ -60,8 +73,8 @@ var_dump($remainder); // string(13) "and more text" > ```php > $parser->parse('int and more text'); > // TypeLang\Parser\Exception\UnexpectedTokenException: -> // Syntax error, unexpected "and " in "int and more text" -> // at column 5 +> // Syntax error, unexpected "and " (T_NAME_WITH_SPACE), +> // T_NS_DELIMITER expected in "int and more text" > ``` > {style="note"} @@ -82,7 +95,7 @@ $content = <<<'PHP' $content = substr($content, strlen('@return ')); // "Example Returns something very interesting!" -$result = $parser->parseTolerant($content); +$result = $parser->partial($content); var_dump($result->type); ``` @@ -110,17 +123,17 @@ The description can then be recovered from the reported offset: $description = substr($content, $result->offset); var_dump($description); -// string(36) "Returns something very interesting!" +// string(35) "Returns something very interesting!" ``` ## Recovering from Incomplete Constructs -Tolerant mode does not attempt to *repair* broken syntax — it stops at the +Partial parsing does not attempt to *repair* broken syntax — it stops at the last point where a complete sub-rule was matched and treats everything past it as trailing content, however implausible that trailing content looks. ```php -$result = $parser->parseTolerant('array{'); +$result = $parser->partial('array{'); var_dump($result->type->name->toString()); // string(5) "array" var_dump($result->offset); // int(5) @@ -129,3 +142,57 @@ var_dump($result->offset); // int(5) Here the unterminated `{` of the shape-fields list is never entered, so the parser falls back to the last valid statement — the bare `array` named type — and reports offset `5`, right before the `{`. + +## Validation + +Where the type itself is of no use and the only question is whether the +source is one, `TypeParser::validate()` asks exactly that and builds nothing. +It returns a `TypeLang\Parser\Validation\CheckResult`, and which of the three +it is answers the question. + + + + +The source is a type whole, so nothing stands in the way of it. + + + + +The source is no type of its own. Carries the `$message` of what stands in +the way, the `$position` (line and column) and the `$offset` the reading +stopped at. + + + + +The grammar described the beginning of the source alone. This is a failure as +well, since a check asks about the source whole, and the rest of it begins at +the `$offset`. + + + + +```php +use TypeLang\Parser\TypeParser; +use TypeLang\Parser\Validation\SuccessfulCheckResult; + +$parser = new TypeParser(); + +var_dump($parser->validate('array{ field: result }')::class); +// TypeLang\Parser\Validation\SuccessfulCheckResult + +var_dump($parser->validate('array{ field: result } and more')::class); +// TypeLang\Parser\Validation\PartialCheckResult + +var_dump($parser->validate('array{')::class); +// TypeLang\Parser\Validation\PartialCheckResult + +// A check for a source that is a type whole: +$isType = $parser->validate($source) instanceof SuccessfulCheckResult; +``` + +> A `PartialCheckResult` **is** a `FailureCheckResult`, and a +> `PartialParsedResult` **is** a `SuccessfulParsedResult`. Each hierarchy is +> shaped around the question its method asks: a check asks about the source +> whole, whereas a partial parse is content with the beginning of it. +> {style="note"} diff --git a/docs/Writerside/topics/phpdoc.md b/docs/Writerside/topics/phpdoc.md index 308f0e7..f0b79c1 100644 --- a/docs/Writerside/topics/phpdoc.md +++ b/docs/Writerside/topics/phpdoc.md @@ -28,7 +28,7 @@ comment. **Requirements:** -* `PHP >= 8.4` +* `PHP >= 8.1` * `ext-mbstring` optional diff --git a/docs/Writerside/topics/phpdoc/combinators.md b/docs/Writerside/topics/phpdoc/combinators.md index 901c5c6..10b3cd5 100644 --- a/docs/Writerside/topics/phpdoc/combinators.md +++ b/docs/Writerside/topics/phpdoc/combinators.md @@ -43,7 +43,7 @@ whatever is left entirely. ## Built-in Combinators -Fourteen combinators cover every built-in tag's grammar between them: +Fifteen combinators cover every built-in tag's grammar between them: @@ -56,6 +56,13 @@ Fourteen combinators cover every built-in tag's grammar between them: + + + + + diff --git a/docs/Writerside/topics/phpdoc/tags/phan-assert-if-false-tag.md b/docs/Writerside/topics/phpdoc/tags/phan-assert-if-false-tag.md index b28a6ca..cbb8279 100644 --- a/docs/Writerside/topics/phpdoc/tags/phan-assert-if-false-tag.md +++ b/docs/Writerside/topics/phpdoc/tags/phan-assert-if-false-tag.md @@ -9,15 +9,16 @@ and its counterpart [@phan-assert-if-true](phan-assert-if-true-tag.md). ``` -"@phan-assert-if-false" [ ] +"@phan-assert-if-false" [ "!" | "=" | "!=" ] [ ] ``` Parsing a `@phan-assert-if-false` tag produces an `AssertIfFalseTag` instance, carrying the -asserted `$type` and `$variable` alongside the `$name` and optional -`$description` every [Tag](phpdoc.md#tag) already provides. +asserted `$type`, the `$subject` it is asserted of and the `$operator` +relating them, alongside the `$name` and optional `$description` every +[Tag](phpdoc.md#tag) already provides. ```php -final class AssertIfFalseTag extends TypedVariableTag {} +final class AssertIfFalseTag extends AssertionTag {} ``` The same `AssertIfFalseTag` is produced by [@psalm-assert-if-false](psalm-assert-if-false-tag.md) and [@phpstan-assert-if-false](phpstan-assert-if-false-tag.md), which restate the same diff --git a/docs/Writerside/topics/phpdoc/tags/phan-assert-if-true-tag.md b/docs/Writerside/topics/phpdoc/tags/phan-assert-if-true-tag.md index 16caa52..33a49a1 100644 --- a/docs/Writerside/topics/phpdoc/tags/phan-assert-if-true-tag.md +++ b/docs/Writerside/topics/phpdoc/tags/phan-assert-if-true-tag.md @@ -9,15 +9,16 @@ and its counterpart [@phan-assert-if-false](phan-assert-if-false-tag.md). ``` -"@phan-assert-if-true" [ ] +"@phan-assert-if-true" [ "!" | "=" | "!=" ] [ ] ``` Parsing a `@phan-assert-if-true` tag produces an `AssertIfTrueTag` instance, carrying the -asserted `$type` and `$variable` alongside the `$name` and optional -`$description` every [Tag](phpdoc.md#tag) already provides. +asserted `$type`, the `$subject` it is asserted of and the `$operator` +relating them, alongside the `$name` and optional `$description` every +[Tag](phpdoc.md#tag) already provides. ```php -final class AssertIfTrueTag extends TypedVariableTag {} +final class AssertIfTrueTag extends AssertionTag {} ``` The same `AssertIfTrueTag` is produced by [@psalm-assert-if-true](psalm-assert-if-true-tag.md) and [@phpstan-assert-if-true](phpstan-assert-if-true-tag.md), which restate the same diff --git a/docs/Writerside/topics/phpdoc/tags/phan-assert-tag.md b/docs/Writerside/topics/phpdoc/tags/phan-assert-tag.md index b1bdf0b..e2b475a 100644 --- a/docs/Writerside/topics/phpdoc/tags/phan-assert-tag.md +++ b/docs/Writerside/topics/phpdoc/tags/phan-assert-tag.md @@ -10,15 +10,16 @@ siblings — see [@phan-assert-if-true](phan-assert-if-true-tag.md) and same assertion family. ``` -"@phan-assert" [ ] +"@phan-assert" [ "!" | "=" | "!=" ] [ ] ``` Parsing a `@phan-assert` tag produces an `AssertTag` instance, carrying the -asserted `$type` and `$variable` alongside the `$name` and optional -`$description` every [Tag](phpdoc.md#tag) already provides. +asserted `$type`, the `$subject` it is asserted of and the `$operator` +relating them, alongside the `$name` and optional `$description` every +[Tag](phpdoc.md#tag) already provides. ```php -final class AssertTag extends TypedVariableTag {} +final class AssertTag extends AssertionTag {} ``` The same `AssertTag` is produced by [@psalm-assert](psalm-assert-tag.md) and [@phpstan-assert](phpstan-assert-tag.md), which restate the same diff --git a/docs/Writerside/topics/phpdoc/tags/phpstan-assert-if-false-tag.md b/docs/Writerside/topics/phpdoc/tags/phpstan-assert-if-false-tag.md index f53c00c..bf000f8 100644 --- a/docs/Writerside/topics/phpdoc/tags/phpstan-assert-if-false-tag.md +++ b/docs/Writerside/topics/phpdoc/tags/phpstan-assert-if-false-tag.md @@ -7,15 +7,16 @@ function returns `false`, defined by PHPStan. It is one of the [@phpstan-assert](phpstan-assert-tag.md) family of narrowing tags. ``` -"@phpstan-assert-if-false" [ ] +"@phpstan-assert-if-false" [ "!" | "=" | "!=" ] [ ] ``` Parsing a `@phpstan-assert-if-false` tag produces an `AssertIfFalseTag` instance, carrying the -asserted `$type` and `$variable` alongside the `$name` and optional -`$description` every [Tag](phpdoc.md#tag) already provides. +asserted `$type`, the `$subject` it is asserted of and the `$operator` +relating them, alongside the `$name` and optional `$description` every +[Tag](phpdoc.md#tag) already provides. ```php -final class AssertIfFalseTag extends TypedVariableTag {} +final class AssertIfFalseTag extends AssertionTag {} ``` The same `AssertIfFalseTag` is produced by [@psalm-assert-if-false](psalm-assert-if-false-tag.md) and [@phan-assert-if-false](phan-assert-if-false-tag.md), which restate the same diff --git a/docs/Writerside/topics/phpdoc/tags/phpstan-assert-if-true-tag.md b/docs/Writerside/topics/phpdoc/tags/phpstan-assert-if-true-tag.md index b4fce07..f9149ee 100644 --- a/docs/Writerside/topics/phpdoc/tags/phpstan-assert-if-true-tag.md +++ b/docs/Writerside/topics/phpdoc/tags/phpstan-assert-if-true-tag.md @@ -7,15 +7,16 @@ function returns `true`, defined by PHPStan. It is one of the [@phpstan-assert](phpstan-assert-tag.md) family of narrowing tags. ``` -"@phpstan-assert-if-true" [ ] +"@phpstan-assert-if-true" [ "!" | "=" | "!=" ] [ ] ``` Parsing a `@phpstan-assert-if-true` tag produces an `AssertIfTrueTag` instance, carrying the -asserted `$type` and `$variable` alongside the `$name` and optional -`$description` every [Tag](phpdoc.md#tag) already provides. +asserted `$type`, the `$subject` it is asserted of and the `$operator` +relating them, alongside the `$name` and optional `$description` every +[Tag](phpdoc.md#tag) already provides. ```php -final class AssertIfTrueTag extends TypedVariableTag {} +final class AssertIfTrueTag extends AssertionTag {} ``` The same `AssertIfTrueTag` is produced by [@psalm-assert-if-true](psalm-assert-if-true-tag.md) and [@phan-assert-if-true](phan-assert-if-true-tag.md), which restate the same diff --git a/docs/Writerside/topics/phpdoc/tags/phpstan-assert-tag.md b/docs/Writerside/topics/phpdoc/tags/phpstan-assert-tag.md index 8f0b35c..ac1a469 100644 --- a/docs/Writerside/topics/phpdoc/tags/phpstan-assert-tag.md +++ b/docs/Writerside/topics/phpdoc/tags/phpstan-assert-tag.md @@ -8,15 +8,16 @@ narrowed to a given type after the call, defined by PHPStan. It has boolean-returning function returns a particular value. ``` -"@phpstan-assert" [ ] +"@phpstan-assert" [ "!" | "=" | "!=" ] [ ] ``` Parsing a `@phpstan-assert` tag produces an `AssertTag` instance, carrying the -asserted `$type` and `$variable` alongside the `$name` and optional -`$description` every [Tag](phpdoc.md#tag) already provides. +asserted `$type`, the `$subject` it is asserted of and the `$operator` +relating them, alongside the `$name` and optional `$description` every +[Tag](phpdoc.md#tag) already provides. ```php -final class AssertTag extends TypedVariableTag {} +final class AssertTag extends AssertionTag {} ``` The same `AssertTag` is produced by [@psalm-assert](psalm-assert-tag.md) and [@phan-assert](phan-assert-tag.md), which restate the same diff --git a/docs/Writerside/topics/phpdoc/tags/psalm-assert-if-false-tag.md b/docs/Writerside/topics/phpdoc/tags/psalm-assert-if-false-tag.md index b701939..643d082 100644 --- a/docs/Writerside/topics/phpdoc/tags/psalm-assert-if-false-tag.md +++ b/docs/Writerside/topics/phpdoc/tags/psalm-assert-if-false-tag.md @@ -8,15 +8,16 @@ defined by the static analyzer Psalm, alongside [@psalm-assert](psalm-assert-tag.md). ``` -"@psalm-assert-if-false" [ ] +"@psalm-assert-if-false" [ "!" | "=" | "!=" ] [ ] ``` Parsing a `@psalm-assert-if-false` tag produces an `AssertIfFalseTag` instance, carrying the -asserted `$type` and `$variable` alongside the `$name` and optional -`$description` every [Tag](phpdoc.md#tag) already provides. +asserted `$type`, the `$subject` it is asserted of and the `$operator` +relating them, alongside the `$name` and optional `$description` every +[Tag](phpdoc.md#tag) already provides. ```php -final class AssertIfFalseTag extends TypedVariableTag {} +final class AssertIfFalseTag extends AssertionTag {} ``` The same `AssertIfFalseTag` is produced by [@phpstan-assert-if-false](phpstan-assert-if-false-tag.md) and [@phan-assert-if-false](phan-assert-if-false-tag.md), which restate the same diff --git a/docs/Writerside/topics/phpdoc/tags/psalm-assert-if-true-tag.md b/docs/Writerside/topics/phpdoc/tags/psalm-assert-if-true-tag.md index e60f763..0ba0d99 100644 --- a/docs/Writerside/topics/phpdoc/tags/psalm-assert-if-true-tag.md +++ b/docs/Writerside/topics/phpdoc/tags/psalm-assert-if-true-tag.md @@ -8,15 +8,16 @@ defined by the static analyzer Psalm, alongside [@psalm-assert](psalm-assert-tag.md). ``` -"@psalm-assert-if-true" [ ] +"@psalm-assert-if-true" [ "!" | "=" | "!=" ] [ ] ``` Parsing a `@psalm-assert-if-true` tag produces an `AssertIfTrueTag` instance, carrying the -asserted `$type` and `$variable` alongside the `$name` and optional -`$description` every [Tag](phpdoc.md#tag) already provides. +asserted `$type`, the `$subject` it is asserted of and the `$operator` +relating them, alongside the `$name` and optional `$description` every +[Tag](phpdoc.md#tag) already provides. ```php -final class AssertIfTrueTag extends TypedVariableTag {} +final class AssertIfTrueTag extends AssertionTag {} ``` The same `AssertIfTrueTag` is produced by [@phpstan-assert-if-true](phpstan-assert-if-true-tag.md) and [@phan-assert-if-true](phan-assert-if-true-tag.md), which restate the same diff --git a/docs/Writerside/topics/phpdoc/tags/psalm-assert-tag.md b/docs/Writerside/topics/phpdoc/tags/psalm-assert-tag.md index d1a2c27..798f8f0 100644 --- a/docs/Writerside/topics/phpdoc/tags/psalm-assert-tag.md +++ b/docs/Writerside/topics/phpdoc/tags/psalm-assert-tag.md @@ -7,15 +7,16 @@ narrowed to a given type after the call returns. It is defined by the static analyzer Psalm. ``` -"@psalm-assert" [ ] +"@psalm-assert" [ "!" | "=" | "!=" ] [ ] ``` Parsing a `@psalm-assert` tag produces an `AssertTag` instance, carrying the -asserted `$type` and `$variable` alongside the `$name` and optional -`$description` every [Tag](phpdoc.md#tag) already provides. +asserted `$type`, the `$subject` it is asserted of and the `$operator` +relating them, alongside the `$name` and optional `$description` every +[Tag](phpdoc.md#tag) already provides. ```php -final class AssertTag extends TypedVariableTag {} +final class AssertTag extends AssertionTag {} ``` The same `AssertTag` is produced by [@phpstan-assert](phpstan-assert-tag.md) and [@phan-assert](phan-assert-tag.md), which restate the same diff --git a/docs/Writerside/topics/printer.md b/docs/Writerside/topics/printer.md index 1c95d77..caa6646 100644 --- a/docs/Writerside/topics/printer.md +++ b/docs/Writerside/topics/printer.md @@ -38,7 +38,7 @@ It ships with two printers that render the very same AST differently: **Requirements:** -* `PHP >= 8.4` +* `PHP >= 8.1` * `ext-mbstring` optional ## Quick Start diff --git a/docs/Writerside/topics/reader.md b/docs/Writerside/topics/reader.md index 6874963..d9c5270 100644 --- a/docs/Writerside/topics/reader.md +++ b/docs/Writerside/topics/reader.md @@ -35,7 +35,7 @@ phpdoc is the job of the PHPDoc parser. **Requirements:** -* `PHP >= 8.4` +* `PHP >= 8.1` * `ext-mbstring` optional ## Quick Start diff --git a/libs/parser/.gitattributes b/libs/parser/.gitattributes index cfa4eea..11aad01 100644 --- a/libs/parser/.gitattributes +++ b/libs/parser/.gitattributes @@ -1,6 +1,7 @@ * text=auto eol=lf *.pp eol=lf linguist-language=EBNF *.pp2 eol=lf linguist-language=EBNF +*.pp3 eol=lf linguist-language=EBNF # GIT .editorconfig export-ignore diff --git a/libs/parser/.github/workflows/tests.yml b/libs/parser/.github/workflows/tests.yml index a7df15d..3abc5cb 100644 --- a/libs/parser/.github/workflows/tests.yml +++ b/libs/parser/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - php: [ '8.4', '8.5' ] + php: [ '8.1', '8.2', '8.3', '8.4', '8.5' ] os: [ ubuntu-latest, macos-latest, windows-latest ] stability: [ lowest, stable ] steps: diff --git a/libs/parser/README.md b/libs/parser/README.md index f2a0433..308d97f 100644 --- a/libs/parser/README.md +++ b/libs/parser/README.md @@ -33,7 +33,7 @@ composer require type-lang/parser ``` **Requirements:** -- PHP 8.4+ +- PHP 8.1+ ## Usage diff --git a/libs/parser/bin/build b/libs/parser/bin/build index 4ab89a7..accea66 100644 --- a/libs/parser/bin/build +++ b/libs/parser/bin/build @@ -4,6 +4,9 @@ declare(strict_types=1); use Phplrt\Compiler\Compiler; +use Phplrt\Compiler\Generator\ClassModifier; +use Phplrt\Compiler\Generator\TargetPhpVersion; +use Phplrt\Source\FileSource; if (!in_array(PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) { trigger_error('The console should be invoked via the CLI ' @@ -31,20 +34,16 @@ if (!class_exists(Compiler::class)) { exit(-1); } -$grammar = (new Compiler()) - ->load(new SplFileInfo(__DIR__ . '/../resources/grammar.pp2')) - ->build() - ->withClassReference('TypeLang\\Type') - ->withClassReference('TypeLang\\Parser\\Exception') - ->generate(); - -file_put_contents(__DIR__ . '/../resources/grammar.php', $grammar); - -// -// Postprocess and optimize output grammar -// - -$data = file_get_contents(__DIR__ . '/../resources/grammar.php'); - -// Replace functions to static one -file_put_contents(__DIR__ . '/../resources/grammar.php', $data); +(new Compiler()) + ->load(FileSource::createFromPathname(__DIR__ . '/../resources/grammar.pp3')) + ->generate() + ->withNamespaceName('TypeLang\\Parser\\Internal') + ->withClassName('CompiledExecutor') + ->withClassModifier(ClassModifier::Abstract) + // The parser carries the features it recognizes a source with, so it + // is declared as a mutable class rather than a readonly one. + ->withReadonly(false) + ->withTargetPhpVersion(TargetPhpVersion::Php81) + ->withClassImport('TypeLang\\Type') + ->withClassImport('TypeLang\\Parser\\Exception') + ->save(__DIR__ . '/../src/Internal/CompiledExecutor.php'); diff --git a/libs/parser/composer.json b/libs/parser/composer.json index bc78049..2813faa 100644 --- a/libs/parser/composer.json +++ b/libs/parser/composer.json @@ -9,11 +9,9 @@ "issues": "https://github.com/php-type-language/parser/issues" }, "require": { - "php": "^8.4", + "php": "^8.1", "type-lang/types": "^2.0", - "phplrt/lexer": "^3.7", - "phplrt/parser": "^3.7", - "phplrt/source": "^3.7" + "phplrt/runtime": "^4.1" }, "autoload": { "psr-4": { @@ -22,9 +20,9 @@ }, "require-dev": { "jetbrains/phpstorm-attributes": "^1.2", - "phplrt/compiler": "^3.7.5", - "phpstan/phpstan": "^2.2.5", - "phpunit/phpunit": "^13.2.4" + "phplrt/compiler": "^4.1", + "phpstan/phpstan": "^2.2", + "phpunit/phpunit": "^10.5|^11.0|^12.0|^13.0" }, "autoload-dev": { "psr-4": { diff --git a/libs/parser/phpstan.neon b/libs/parser/phpstan.neon index 018e46e..bf3ffa2 100644 --- a/libs/parser/phpstan.neon +++ b/libs/parser/phpstan.neon @@ -1,6 +1,16 @@ parameters: level: max + phpVersion: + min: 80100 + max: 80599 + treatPhpDocTypesAsCertain: false fileExtensions: - php paths: - src + excludePaths: + analyse: + # The parser is generated out of the grammar by the phplrt compiler and + # is not written by hand. + - src/Internal/CompiledExecutor.php + reportUnmatchedIgnoredErrors: false diff --git a/libs/parser/resources/grammar.php b/libs/parser/resources/grammar.php deleted file mode 100644 index 408de44..0000000 --- a/libs/parser/resources/grammar.php +++ /dev/null @@ -1,804 +0,0 @@ -, - * ... - * }, - * skip: list, - * grammar: array, - * reducers: array, - * transitions?: array - * } - */ -return [ - 'initial' => 59, - 'tokens' => [ - 'default' => [ - 'T_DQ_STRING_LITERAL' => '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"', - 'T_SQ_STRING_LITERAL' => '\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'', - 'T_PFX_FLOAT_LITERAL' => '\\-?[0-9]++\\.[0-9]*+(?:[eE]-?[0-9]++)?', - 'T_SFX_FLOAT_LITERAL' => '\\-?[0-9]*+\\.[0-9]++(?:[eE]-?[0-9]++)?', - 'T_EXP_LITERAL' => '\\-?[0-9]++[eE]-?[0-9]++', - 'T_BIN_INT_LITERAL' => '\\-?0[bB][01_]++', - 'T_OCT_INT_LITERAL' => '\\-?0[oO][0-7_]++', - 'T_HEX_INT_LITERAL' => '\\-?0[xX][0-9a-fA-F_]++', - 'T_DEC_INT_LITERAL' => '\\-?[0-9][0-9_]*+', - 'T_BOOL_LITERAL' => '(?i)(?:true|false)(?![a-zA-Z0-9\\-_\\x80-\\xff])', - 'T_NULL_LITERAL' => '(?i)null(?![a-zA-Z0-9\\-_\\x80-\\xff])', - 'T_NEQ' => '(?i)is\\h++not(?![a-zA-Z0-9\\-_\\x80-\\xff])', - 'T_EQ' => '(?i)is(?![a-zA-Z0-9\\-_\\x80-\\xff])', - 'T_THIS' => '\\$this\\b', - 'T_VARIABLE' => '\\$[a-zA-Z_\\x80-\\xff][a-zA-Z0-9\\-_\\x80-\\xff]*+', - 'T_NAME_WITH_SPACE' => '[a-zA-Z_\\x80-\\xff][a-zA-Z0-9\\-_\\x80-\\xff]*+\\s++', - 'T_NAME' => '[a-zA-Z_\\x80-\\xff][a-zA-Z0-9\\-_\\x80-\\xff]*+', - 'T_LTE' => '<=', - 'T_GTE' => '>=', - 'T_ANGLE_BRACKET_OPEN' => '<', - 'T_ANGLE_BRACKET_CLOSE' => '>', - 'T_PARENTHESIS_OPEN' => '\\(', - 'T_PARENTHESIS_CLOSE' => '\\)', - 'T_BRACE_OPEN' => '\\{', - 'T_BRACE_CLOSE' => '\\}', - 'T_ATTR_OPEN' => '#\\[', - 'T_SQUARE_BRACKET_OPEN' => '\\[', - 'T_SQUARE_BRACKET_CLOSE' => '\\]', - 'T_COMMA' => ',', - 'T_ELLIPSIS' => '\\.\\.\\.', - 'T_DOUBLE_COLON' => '::', - 'T_COLON' => ':', - 'T_ASSIGN' => '=', - 'T_NS_DELIMITER' => '\\\\', - 'T_QMARK' => '\\?', - 'T_OR' => '\\|', - 'T_AMP' => '&', - 'T_ASTERISK' => '\\*', - 'T_COMMENT' => '(?://|#)[^\\r\\n]*+', - 'T_DOC_COMMENT' => '/\\*.*?\\*/', - 'T_WHITESPACE' => '\\s++', - ], - ], - 'skip' => [ - 'T_COMMENT', - 'T_DOC_COMMENT', - 'T_WHITESPACE', - ], - 'transitions' => [], - 'grammar' => [ - new Phplrt\Parser\Grammar\Concatenation([6, 3, 7]), - new Phplrt\Parser\Grammar\Concatenation([3, 10]), - new Phplrt\Parser\Grammar\Alternation([0, 1]), - new Phplrt\Parser\Grammar\Alternation([11, 12, 13, 14, 15]), - new Phplrt\Parser\Grammar\Lexeme('T_NS_DELIMITER', false), - new Phplrt\Parser\Grammar\Concatenation([4, 3]), - new Phplrt\Parser\Grammar\Lexeme('T_NS_DELIMITER', false), - new Phplrt\Parser\Grammar\Repetition(5, 0, INF), - new Phplrt\Parser\Grammar\Lexeme('T_NS_DELIMITER', false), - new Phplrt\Parser\Grammar\Concatenation([8, 3]), - new Phplrt\Parser\Grammar\Repetition(9, 0, INF), - new Phplrt\Parser\Grammar\Lexeme('T_NAME', true), - new Phplrt\Parser\Grammar\Lexeme('T_NAME_WITH_SPACE', true), - new Phplrt\Parser\Grammar\Lexeme('T_EQ', true), - new Phplrt\Parser\Grammar\Lexeme('T_BOOL_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_NULL_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_NAME_WITH_SPACE', true), - new Phplrt\Parser\Grammar\Alternation([21, 22, 23, 24, 25]), - new Phplrt\Parser\Grammar\Concatenation([2, 39]), - new Phplrt\Parser\Grammar\Concatenation([2, 43, 44]), - new Phplrt\Parser\Grammar\Alternation([17, 18, 19]), - new Phplrt\Parser\Grammar\Alternation([30, 31]), - new Phplrt\Parser\Grammar\Alternation([32, 33, 34]), - new Phplrt\Parser\Grammar\Alternation([35, 36, 37, 38]), - new Phplrt\Parser\Grammar\Lexeme('T_BOOL_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_NULL_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_VARIABLE', true), - new Phplrt\Parser\Grammar\Lexeme('T_THIS', true), - new Phplrt\Parser\Grammar\Alternation([26, 27]), - new Phplrt\Parser\Grammar\Lexeme('T_THIS', true), - new Phplrt\Parser\Grammar\Lexeme('T_DQ_STRING_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_SQ_STRING_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_PFX_FLOAT_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_SFX_FLOAT_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_EXP_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_BIN_INT_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_OCT_INT_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_HEX_INT_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_DEC_INT_LITERAL', true), - new Phplrt\Parser\Grammar\Lexeme('T_ASTERISK', false), - new Phplrt\Parser\Grammar\Lexeme('T_ASTERISK', true), - new Phplrt\Parser\Grammar\Concatenation([3, 40]), - new Phplrt\Parser\Grammar\Lexeme('T_ASTERISK', true), - new Phplrt\Parser\Grammar\Lexeme('T_DOUBLE_COLON', false), - new Phplrt\Parser\Grammar\Alternation([41, 3, 42]), - new Phplrt\Parser\Grammar\Concatenation([57, 58]), - new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), - new Phplrt\Parser\Grammar\Concatenation([46, 45]), - new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), - new Phplrt\Parser\Grammar\Lexeme('T_ANGLE_BRACKET_OPEN', false), - new Phplrt\Parser\Grammar\Repetition(47, 0, INF), - new Phplrt\Parser\Grammar\Optional(48), - new Phplrt\Parser\Grammar\Lexeme('T_ANGLE_BRACKET_CLOSE', false), - new Phplrt\Parser\Grammar\Concatenation([49, 45, 50, 51, 52]), - new Phplrt\Parser\Grammar\Repetition(155, 1, INF), - new Phplrt\Parser\Grammar\Concatenation([16, 59]), - new Phplrt\Parser\Grammar\Concatenation([59]), - new Phplrt\Parser\Grammar\Optional(54), - new Phplrt\Parser\Grammar\Alternation([55, 56]), - new Phplrt\Parser\Grammar\Concatenation([175]), - new Phplrt\Parser\Grammar\Concatenation([67, 71, 72]), - new Phplrt\Parser\Grammar\Concatenation([107, 59]), - new Phplrt\Parser\Grammar\Lexeme('T_PARENTHESIS_OPEN', false), - new Phplrt\Parser\Grammar\Optional(60), - new Phplrt\Parser\Grammar\Lexeme('T_PARENTHESIS_CLOSE', false), - new Phplrt\Parser\Grammar\Optional(61), - new Phplrt\Parser\Grammar\Concatenation([2, 62, 63, 64, 65]), - new Phplrt\Parser\Grammar\Concatenation([74, 73]), - new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), - new Phplrt\Parser\Grammar\Concatenation([68, 67]), - new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), - new Phplrt\Parser\Grammar\Repetition(69, 0, INF), - new Phplrt\Parser\Grammar\Optional(70), - new Phplrt\Parser\Grammar\Concatenation([78, 79]), - new Phplrt\Parser\Grammar\Optional(54), - new Phplrt\Parser\Grammar\Concatenation([93, 92]), - new Phplrt\Parser\Grammar\Alternation([83, 86, 88, 90, 80]), - new Phplrt\Parser\Grammar\Lexeme('T_ASSIGN', true), - new Phplrt\Parser\Grammar\Alternation([75, 76]), - new Phplrt\Parser\Grammar\Optional(77), - new Phplrt\Parser\Grammar\Concatenation([28]), - new Phplrt\Parser\Grammar\Lexeme('T_AMP', true), - new Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true), - new Phplrt\Parser\Grammar\Concatenation([81, 82, 80]), - new Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true), - new Phplrt\Parser\Grammar\Lexeme('T_AMP', true), - new Phplrt\Parser\Grammar\Concatenation([84, 85, 80]), - new Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true), - new Phplrt\Parser\Grammar\Concatenation([87, 80]), - new Phplrt\Parser\Grammar\Lexeme('T_AMP', true), - new Phplrt\Parser\Grammar\Concatenation([89, 80]), - new Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true), - new Phplrt\Parser\Grammar\Concatenation([94, 95]), - new Phplrt\Parser\Grammar\Optional(91), - new Phplrt\Parser\Grammar\Concatenation([96, 106]), - new Phplrt\Parser\Grammar\Optional(28), - new Phplrt\Parser\Grammar\Concatenation([59]), - new Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true), - new Phplrt\Parser\Grammar\Lexeme('T_AMP', true), - new Phplrt\Parser\Grammar\Optional(97), - new Phplrt\Parser\Grammar\Concatenation([98, 99]), - new Phplrt\Parser\Grammar\Lexeme('T_AMP', true), - new Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true), - new Phplrt\Parser\Grammar\Optional(101), - new Phplrt\Parser\Grammar\Concatenation([102, 103]), - new Phplrt\Parser\Grammar\Alternation([100, 104]), - new Phplrt\Parser\Grammar\Optional(105), - new Phplrt\Parser\Grammar\Lexeme('T_COLON', false), - new Phplrt\Parser\Grammar\Concatenation([123, 126]), - new Phplrt\Parser\Grammar\Concatenation([121, 122]), - new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), - new Phplrt\Parser\Grammar\Concatenation([110, 109]), - new Phplrt\Parser\Grammar\Optional(111), - new Phplrt\Parser\Grammar\Concatenation([108, 112]), - new Phplrt\Parser\Grammar\Optional(109), - new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), - new Phplrt\Parser\Grammar\Lexeme('T_BRACE_OPEN', false), - new Phplrt\Parser\Grammar\Alternation([113, 114]), - new Phplrt\Parser\Grammar\Optional(115), - new Phplrt\Parser\Grammar\Lexeme('T_BRACE_CLOSE', false), - new Phplrt\Parser\Grammar\Concatenation([116, 117, 118, 119]), - new Phplrt\Parser\Grammar\Lexeme('T_ELLIPSIS', true), - new Phplrt\Parser\Grammar\Optional(53), - new Phplrt\Parser\Grammar\Concatenation([129, 130]), - new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), - new Phplrt\Parser\Grammar\Concatenation([124, 123]), - new Phplrt\Parser\Grammar\Repetition(125, 0, INF), - new Phplrt\Parser\Grammar\Concatenation([131, 134, 135, 133]), - new Phplrt\Parser\Grammar\Concatenation([133]), - new Phplrt\Parser\Grammar\Optional(54), - new Phplrt\Parser\Grammar\Alternation([127, 128]), - new Phplrt\Parser\Grammar\Alternation([18, 19, 3, 23, 21]), - new Phplrt\Parser\Grammar\Lexeme('T_QMARK', true), - new Phplrt\Parser\Grammar\Concatenation([59]), - new Phplrt\Parser\Grammar\Optional(132), - new Phplrt\Parser\Grammar\Lexeme('T_COLON', false), - new Phplrt\Parser\Grammar\Alternation([53, 120]), - new Phplrt\Parser\Grammar\Optional(136), - new Phplrt\Parser\Grammar\Concatenation([2, 137]), - new Phplrt\Parser\Grammar\Concatenation([176]), - new Phplrt\Parser\Grammar\Optional(142), - new Phplrt\Parser\Grammar\Concatenation([139, 140]), - new Phplrt\Parser\Grammar\Concatenation([145, 146, 147, 59, 148, 59]), - new Phplrt\Parser\Grammar\Concatenation([28, 142]), - new Phplrt\Parser\Grammar\Alternation([141, 143]), - new Phplrt\Parser\Grammar\Alternation([149, 150, 151, 152, 153, 154]), - new Phplrt\Parser\Grammar\Alternation([59, 28]), - new Phplrt\Parser\Grammar\Lexeme('T_QMARK', false), - new Phplrt\Parser\Grammar\Lexeme('T_COLON', false), - new Phplrt\Parser\Grammar\Lexeme('T_EQ', true), - new Phplrt\Parser\Grammar\Lexeme('T_NEQ', true), - new Phplrt\Parser\Grammar\Lexeme('T_GTE', true), - new Phplrt\Parser\Grammar\Lexeme('T_LTE', true), - new Phplrt\Parser\Grammar\Lexeme('T_ANGLE_BRACKET_OPEN', true), - new Phplrt\Parser\Grammar\Lexeme('T_ANGLE_BRACKET_CLOSE', true), - new Phplrt\Parser\Grammar\Concatenation([158, 156, 159, 160]), - new Phplrt\Parser\Grammar\Concatenation([161, 164]), - new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), - new Phplrt\Parser\Grammar\Lexeme('T_ATTR_OPEN', false), - new Phplrt\Parser\Grammar\Optional(157), - new Phplrt\Parser\Grammar\Lexeme('T_SQUARE_BRACKET_CLOSE', false), - new Phplrt\Parser\Grammar\Concatenation([2, 166]), - new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), - new Phplrt\Parser\Grammar\Concatenation([162, 161]), - new Phplrt\Parser\Grammar\Repetition(163, 0, INF), - new Phplrt\Parser\Grammar\Concatenation([171, 167, 172, 173, 174]), - new Phplrt\Parser\Grammar\Optional(165), - new Phplrt\Parser\Grammar\Concatenation([59]), - new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), - new Phplrt\Parser\Grammar\Concatenation([168, 167]), - new Phplrt\Parser\Grammar\Lexeme('T_COMMA', false), - new Phplrt\Parser\Grammar\Lexeme('T_PARENTHESIS_OPEN', false), - new Phplrt\Parser\Grammar\Repetition(169, 0, INF), - new Phplrt\Parser\Grammar\Optional(170), - new Phplrt\Parser\Grammar\Lexeme('T_PARENTHESIS_CLOSE', false), - new Phplrt\Parser\Grammar\Concatenation([144]), - new Phplrt\Parser\Grammar\Concatenation([177, 180]), - new Phplrt\Parser\Grammar\Concatenation([181, 184]), - new Phplrt\Parser\Grammar\Lexeme('T_OR', false), - new Phplrt\Parser\Grammar\Concatenation([178, 176]), - new Phplrt\Parser\Grammar\Optional(179), - new Phplrt\Parser\Grammar\Concatenation([185]), - new Phplrt\Parser\Grammar\Lexeme('T_AMP', false), - new Phplrt\Parser\Grammar\Concatenation([182, 177]), - new Phplrt\Parser\Grammar\Optional(183), - new Phplrt\Parser\Grammar\Alternation([188, 186]), - new Phplrt\Parser\Grammar\Concatenation([189, 191]), - new Phplrt\Parser\Grammar\Lexeme('T_QMARK', true), - new Phplrt\Parser\Grammar\Concatenation([187, 186]), - new Phplrt\Parser\Grammar\Alternation([197, 29, 20, 66, 138]), - new Phplrt\Parser\Grammar\Concatenation([192, 193, 194]), - new Phplrt\Parser\Grammar\Repetition(190, 0, INF), - new Phplrt\Parser\Grammar\Lexeme('T_SQUARE_BRACKET_OPEN', true), - new Phplrt\Parser\Grammar\Optional(59), - new Phplrt\Parser\Grammar\Lexeme('T_SQUARE_BRACKET_CLOSE', false), - new Phplrt\Parser\Grammar\Lexeme('T_PARENTHESIS_OPEN', false), - new Phplrt\Parser\Grammar\Lexeme('T_PARENTHESIS_CLOSE', false), - new Phplrt\Parser\Grammar\Concatenation([195, 59, 196]), - ], - 'reducers' => [ - 0 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\Name($children, true); - }, - 1 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\Name($children, false); - }, - 3 => static function (Phplrt\Parser\Context $ctx, $children) { - return Type\Identifier::createFromString($children->getValue()); - }, - 16 => static function (Phplrt\Parser\Context $ctx, $children) { - return Type\Identifier::createFromString($children->getValue()); - }, - 17 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - if ($this->features->literals === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('literal values', $offset); - } - - return $children; - }, - 18 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\ConstMaskNode($children[0]); - }, - 19 => static function (Phplrt\Parser\Context $ctx, $children) { - // :: "*" - if (\count($children) === 3) { - return new Type\ClassConstMaskNode( - $children[0], - $children[1], - ); - } - - // :: - if ($children[1] instanceof Type\Identifier) { - return new Type\ClassConstNode( - $children[0], - $children[1], - ); - } - - // :: "*" - return new Type\ClassConstMaskNode($children[0]); - }, - 21 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$token" variable is an auto-generated - $token = $ctx->lastProcessedToken; - - return $this->stringPool[$token] ??= $children; - }, - 22 => static function (Phplrt\Parser\Context $ctx, $children) { - // The "$token" variable is an auto-generated - $token = $ctx->lastProcessedToken; - - return Type\Literal\FloatLiteralNode::parse($token->getValue()); - }, - 23 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$token" variable is an auto-generated - $token = $ctx->lastProcessedToken; - - return $this->integerPool[$token] ??= Type\Literal\IntLiteralNode::parse($token->getValue()); - }, - 24 => static function (Phplrt\Parser\Context $ctx, $children) { - // The "$token" variable is an auto-generated - $token = $ctx->lastProcessedToken; - - return Type\Literal\BoolLiteralNode::parse($token->getValue()); - }, - 25 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\Literal\NullLiteralNode($children->getValue()); - }, - 28 => static function (Phplrt\Parser\Context $ctx, $children) { - // The "$token" variable is an auto-generated - $token = $ctx->lastProcessedToken; - - return Type\Literal\VariableLiteralNode::parse($token->getValue()); - }, - 29 => static function (Phplrt\Parser\Context $ctx, $children) { - // The "$token" variable is an auto-generated - $token = $ctx->lastProcessedToken; - - return Type\Literal\VariableLiteralNode::parse($token->getValue()); - }, - 30 => static function (Phplrt\Parser\Context $ctx, $children) { - // The "$token" variable is an auto-generated - $token = $ctx->lastProcessedToken; - - return Type\Literal\StringLiteralNode::createFromDoubleQuotedString($token->getValue()); - }, - 31 => static function (Phplrt\Parser\Context $ctx, $children) { - // The "$token" variable is an auto-generated - $token = $ctx->lastProcessedToken; - - return Type\Literal\StringLiteralNode::createFromSingleQuotedString($token->getValue()); - }, - 45 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - $hint = $attributes = null; - - if (\reset($children) instanceof Type\Attribute\AttributeGroupListNode) { - if ($this->features->attributes === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template argument attributes', $offset); - } - - $attributes = \array_shift($children); - } - - $type = \array_pop($children); - - if (\reset($children) !== false) { - if ($this->features->hints === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template argument hints', $offset); - } - - $hint = \reset($children); - } - - return new Type\Template\TemplateArgumentNode( - $type, - $hint, - $attributes, - ); - }, - 53 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - if ($this->features->generics === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template arguments', $offset); - } - - return new Type\Template\TemplateArgumentListNode($children); - }, - 54 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\Attribute\AttributeGroupListNode($children); - }, - 60 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\Callable\CallableParameterListNode($children); - }, - 66 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - $name = \array_shift($children); - - if ($this->features->callables === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $offset); - } - - $parameters = isset($children[0]) && $children[0] instanceof Type\Callable\CallableParameterListNode - ? \array_shift($children) - : new Type\Callable\CallableParameterListNode(); - - return new Type\CallableTypeNode( - name: $name, - parameters: $parameters, - type: $children[0] ?? null, - ); - }, - 67 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - $result = \end($children); - - if ($children[0] instanceof Type\Attribute\AttributeGroupListNode) { - if ($this->features->attributes === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable parameter attributes', $offset); - } - - $result->attributes = $children[0]; - } - - return $result; - }, - 73 => static function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - if (\count($children) === 1) { - return $children[0]; - } - - if ($children[0]->isVariadic) { - throw Exception\VariadicWithDefaultException::becauseVariadicHasDefault($offset); - } - - $children[0]->isOptional = true; - - return $children[0]; - }, - 75 => static function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - if (\count($children) === 1) { - return $children[0]; - } - - if ($children[1]->isVariadic) { - throw Exception\VariadicRedefinitionException::becauseVariadicIsRedefined($offset); - } - - $children[1]->isVariadic = true; - - return $children[1]; - }, - 76 => static function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - if (!\is_array($children)) { - return $children; - } - - $result = \end($children); - - foreach ($children as $modifier) { - if ($modifier instanceof Phplrt\Contracts\Lexer\TokenInterface) { - switch ($modifier->getName()) { - case 'T_AMP': - $result->isOutput = true; - break; - case 'T_ELLIPSIS': - if ($result->isVariadic) { - throw Exception\VariadicRedefinitionException::becauseVariadicIsRedefined($offset); - } - $result->isVariadic = true; - break; - } - } - } - - return $result; - }, - 80 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\Callable\CallableParameterNode(null, $children[0]); - }, - 92 => static function (Phplrt\Parser\Context $ctx, $children) { - if (\count($children) === 1) { - return $children[0]; - } - - $children[0]->name = $children[1]; - - return $children[0]; - }, - 94 => static function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - $result = \reset($children); - - foreach ($children as $modifier) { - if ($modifier instanceof Phplrt\Contracts\Lexer\TokenInterface) { - switch ($modifier->getName()) { - case 'T_AMP': - $result->isOutput = true; - break; - case 'T_ELLIPSIS': - if ($result->isVariadic) { - throw Exception\VariadicRedefinitionException::becauseVariadicIsRedefined($offset); - } - $result->isVariadic = true; - break; - } - } - } - - return $result; - }, - 96 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\Callable\CallableParameterNode($children[0]); - }, - 108 => static function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - $explicit = []; - $implicit = false; - - foreach ($children as $field) { - if ($field instanceof Type\Shape\ExplicitFieldNode) { - $key = $field->index; - - if (\in_array($key, $explicit, true)) { - throw Exception\ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated($key, $field->offset); - } - - $explicit[] = $key; - } else { - $implicit = true; - } - } - - if ($explicit !== [] && $implicit) { - throw Exception\ShapeKeysMixingException::becauseShapeKeysAreMixed($offset); - } - - return new Type\Shape\FieldsListNode($children); - }, - 120 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - if ($children === []) { - return new Type\Shape\FieldsListNode(); - } - - if ($this->features->shapes === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape fields', $offset); - } - - $parameters = null; - - if (\end($children) instanceof Type\Template\TemplateArgumentListNode) { - $parameters = \array_pop($children); - } - - $fields = \reset($children) instanceof Type\Shape\FieldsListNode - ? \array_shift($children) - : new Type\Shape\FieldsListNode(); - - if ($children !== []) { - $fields->sealed = false; - } - - return \array_filter([$parameters, $fields]); - }, - 123 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - $result = \end($children); - - if ($children[0] instanceof Type\Attribute\AttributeGroupListNode) { - if ($this->features->attributes === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape field attributes', $offset); - } - - $result->attributes = $children[0]; - } - - return $result; - }, - 127 => static function (Phplrt\Parser\Context $ctx, $children) { - $name = $children[0]; - $value = \array_pop($children); - - // In case of "nullable" suffix defined - $optional = \count($children) === 2; - - return match (true) { - $name instanceof Type\Literal\IntLiteralNode - => new Type\Shape\NumericFieldNode($name, $value, $optional), - $name instanceof Type\Literal\StringLiteralNode - => new Type\Shape\StringNamedFieldNode($name, $value, $optional), - $name instanceof Type\ClassConstNode - => new Type\Shape\ClassConstFieldNode($name, $value, $optional), - $name instanceof Type\ClassConstMaskNode - => new Type\Shape\ClassConstMaskFieldNode($name, $value, $optional), - $name instanceof Type\ConstMaskNode - => new Type\Shape\ConstMaskFieldNode($name, $value, $optional), - default => new Type\Shape\NamedFieldNode($name, $value, $optional), - }; - }, - 128 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\Shape\ImplicitFieldNode($children[0]); - }, - 138 => static function (Phplrt\Parser\Context $ctx, $children) { - $fields = $parameters = null; - - // Shape fields - if (\end($children) instanceof Type\Shape\FieldsListNode) { - $fields = \array_pop($children); - } - - // Template parameters - if (\end($children) instanceof Type\Template\TemplateArgumentListNode) { - $parameters = \array_pop($children); - } - - return new Type\NamedTypeNode( - $children[0], - $parameters, - $fields, - ); - }, - 144 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - $count = \count($children); - - if ($count === 1) { - return $children[0]; - } - - if ($this->features->conditions === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('conditional expressions', $offset); - } - - $condition = match ($children[1]->getName()) { - 'T_EQ' => new Type\Condition\EqualConditionNode( - $children[0], - $children[2], - ), - 'T_NEQ' => new Type\Condition\NotEqualConditionNode( - $children[0], - $children[2], - ), - 'T_GTE' => new Type\Condition\GreaterThanOrEqualConditionNode( - $children[0], - $children[2], - ), - 'T_ANGLE_BRACKET_CLOSE' => new Type\Condition\GreaterThanConditionNode( - $children[0], - $children[2], - ), - 'T_LTE' => new Type\Condition\LessThanOrEqualConditionNode( - $children[0], - $children[2], - ), - 'T_ANGLE_BRACKET_OPEN' => new Type\Condition\LessThanConditionNode( - $children[0], - $children[2], - ), - default => throw Exception\InvalidConditionalOperatorException::becauseConditionalOperatorIsInvalid( - $children[1]->getValue(), - $offset, - ), - }; - - return new Type\TernaryExpressionNode( - $condition, - $children[3], - $children[4], - ); - }, - 155 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\Attribute\AttributeGroupNode($children); - }, - 161 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\Attribute\AttributeNode( - $children[0], - ); - }, - 165 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\Attribute\AttributeArgumentListNode($children); - }, - 167 => static function (Phplrt\Parser\Context $ctx, $children) { - return new Type\Attribute\AttributeArgumentNode($children[0]); - }, - 176 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - if (\count($children) === 2) { - if ($this->features->unions === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('union types', $offset); - } - - return new Type\UnionTypeNode($children[0], $children[1]); - } - - return $children; - }, - 177 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - if (\count($children) === 2) { - if ($this->features->intersections === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('intersection types', $offset); - } - - return new Type\IntersectionTypeNode($children[0], $children[1]); - } - - return $children; - }, - 185 => static function (Phplrt\Parser\Context $ctx, $children) { - if (\is_array($children)) { - return new Type\NullableTypeNode($children[1]); - } - - return $children; - }, - 186 => function (Phplrt\Parser\Context $ctx, $children) { - // The "$offset" variable is an auto-generated - $offset = $ctx->lastProcessedToken->getOffset(); - - $statement = \array_shift($children); - - foreach ($children as $child) { - switch (true) { - // In case of list type - case $child === true: - if ($this->features->lists === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('square bracket list types', $offset); - } - - $statement = new Type\TypesListNode($statement); - break; - // In case of offset access type - case $child instanceof Type\TypeNode: - if ($this->features->offsets === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('type offsets', $offset); - } - - $statement = new Type\TypeOffsetAccessNode($statement, $child); - break; - default: - throw Exception\InternalSemanticException::becauseSubNodeIsUnexpected( - \get_debug_type($child), - $offset, - ); - } - } - - return $statement; - }, - 190 => static function (Phplrt\Parser\Context $ctx, $children) { - return $children[1] ?? true; - }, - ], -]; diff --git a/libs/parser/resources/grammar.pp2 b/libs/parser/resources/grammar.pp2 deleted file mode 100644 index 0c02b43..0000000 --- a/libs/parser/resources/grammar.pp2 +++ /dev/null @@ -1,173 +0,0 @@ - -/** - * ----------------------------------------------------------------------------- - * Language Syntax Summary - * ----------------------------------------------------------------------------- - */ - -%include grammar/lexemes // Lexeme/Token Definitions -%include grammar/common // Common Utils -%include grammar/literals // Literal Definitions -%include grammar/template-arguments // T -%include grammar/callable // callable(mixed): void -%include grammar/shape-fields // array { key: int, ... } -%include grammar/named-type // Map, non-empty-string> -%include grammar/ternary // T is A ? B : C -%include grammar/attribute // #[attr] - -%pragma root Type - -/** - * ----------------------------------------------------------------------------- - * Type Statement - * ----------------------------------------------------------------------------- - * - * Constant references to external definitions or - * describe the type in some way. - * - */ - -Type - : Expression() - ; - -/** - * ----------------------------------------------------------------------------- - * Ternary Expression - * ----------------------------------------------------------------------------- - * - * Ternary conditional expressions, like: - * - T is A ? B : C - for equality type conditions. - * - T is not A ? B ? C - for non-equality type conditions. - * - */ - -Expression - : TernaryExpressionOrLogicalType() - ; - - -/** - * ----------------------------------------------------------------------------- - * Logical Statements - * ----------------------------------------------------------------------------- - * - * Logical statements denote union or intersection types, like: - * - A | B | C - for union type references. - * - A & B & C - for intersection type references. - * - */ - -LogicalType - : UnionType() - ; - -UnionType -> { - if (\count($children) === 2) { - if ($this->features->unions === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('union types', $offset); - } - - return new Type\UnionTypeNode($children[0], $children[1]); - } - - return $children; -} - : IntersectionType() (::T_OR:: UnionType())? - ; - -IntersectionType -> { - if (\count($children) === 2) { - if ($this->features->intersections === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('intersection types', $offset); - } - - return new Type\IntersectionTypeNode($children[0], $children[1]); - } - - return $children; -} - : UnaryType() (::T_AMP:: IntersectionType())? - ; - -/** - * ----------------------------------------------------------------------------- - * Unary Statements - * ----------------------------------------------------------------------------- - * - * Unary statements denote terminal types, like: - * - A - for type reference. - * - ?A - for nullable type reference. - * - 'some' - for string literal reference. - * - etc... - * - */ - -UnaryType - : PrefixedNullableType() - ; - -// stmt = ?Type -PrefixedNullableType -> { - if (\is_array($children)) { - return new Type\NullableTypeNode($children[1]); - } - - return $children; -} - : TypesList() - | TypesList() - ; - -TypesList -> { - $statement = \array_shift($children); - - foreach ($children as $child) { - switch (true) { - // In case of list type - case $child === true: - if ($this->features->lists === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('square bracket list types', $offset); - } - - $statement = new Type\TypesListNode($statement); - break; - // In case of offset access type - case $child instanceof Type\TypeNode: - if ($this->features->offsets === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('type offsets', $offset); - } - - $statement = new Type\TypeOffsetAccessNode($statement, $child); - break; - default: - throw Exception\InternalSemanticException::becauseSubNodeIsUnexpected( - \get_debug_type($child), - $offset, - ); - } - } - - return $statement; -} - : PrimaryType() - TypeListOrOffsetSuffix()* - ; - -// - Returns `true` in case of a list type (`T[]`). -// - Returns `NamedTypeNode` in case of an offset access type (`T[Y]`). -TypeListOrOffsetSuffix -> { - return $children[1] ?? true; -} - : - Type()? - ::T_SQUARE_BRACKET_CLOSE:: - ; - -PrimaryType - : ::T_PARENTHESIS_OPEN:: Type() ::T_PARENTHESIS_CLOSE:: - | ThisLiteral() - | Literal() - | CallableType() - | NamedType() - ; diff --git a/libs/parser/resources/grammar.pp3 b/libs/parser/resources/grammar.pp3 new file mode 100644 index 0000000..6b17c1e --- /dev/null +++ b/libs/parser/resources/grammar.pp3 @@ -0,0 +1,209 @@ + +/** + * ----------------------------------------------------------------------------- + * Language Syntax Summary + * ----------------------------------------------------------------------------- + */ + +%include grammar/lexemes +%include grammar/common +%include grammar/literals +%include grammar/template-arguments // T +%include grammar/template-parameters // callable(): void +%include grammar/callable // callable(mixed): void +%include grammar/shape-fields // array { key: int, ... } +%include grammar/named-type // Map, non-empty-string> + +// The lexer reads bytes rather than codepoints, so a "Тип" is a name the way a "Type" is +%pragma lexer.pcre.disable u + +%pragma root Type + +/** + * ----------------------------------------------------------------------------- + * Type Statement + * ----------------------------------------------------------------------------- + */ + +// The logical operators and the condition are read by this very rule rather +// than by a level of their own: every type the reading goes through is read +// through each of those levels, while an operator stands in only a few of +// them. +Type -> { + if (\count($children) === 1) { + return $children[0]; + } + + // The suffix of a condition is the only thing the rule reads that carries + // a flag of its own, which is what the operands are told from + $condition = null; + + for ($i = 1, $count = \count($children); $i < $count; ++$i) { + if (\is_bool($children[$i])) { + $condition = \array_splice($children, $i); + + break; + } + } + + // The operands joined by an ampersand bind tighter, so they are collected + // into a group of their own, and the groups are what a union is made of + $groups = []; + $group = [$children[0]]; + $isUnion = false; + $isIntersection = false; + + for ($i = 1, $count = \count($children); $i < $count; $i += 2) { + if ($children[$i]->id === self::T_OR) { + $isUnion = true; + $groups[] = $group; + $group = [$children[$i + 1]]; + + continue; + } + + $isIntersection = true; + $group[] = $children[$i + 1]; + } + + $groups[] = $group; + + // The operand that binds tighter is the one reported first, the way the + // reading would have reached it + if ($isIntersection && $this->features->intersections === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('intersection types', $source, $offset); + } + + if ($isUnion && $this->features->unions === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('union types', $source, $offset); + } + + $statements = []; + + foreach ($groups as $group) { + $statements[] = \count($group) === 1 + ? $group[0] + : new Type\IntersectionTypeNode($group, $group[0]->offset); + } + + $statement = \count($statements) === 1 + ? $statements[0] + : new Type\UnionTypeNode($statements, $offset); + + if ($condition === null) { + return $statement; + } + + if ($this->features->conditions === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('conditional expressions', $source, $offset); + } + + $comparison = $condition[0] + ? new Type\Condition\NotEqualConditionNode($statement, $condition[1], $offset) + : new Type\Condition\EqualConditionNode($statement, $condition[1], $offset); + + return new Type\TernaryExpressionNode( + $comparison, + $condition[2], + $condition[3], + $offset, + ); +} + : UnaryType() LogicalTail()* TernaryExpressionSuffix()? + | Variable() TernaryExpressionSuffix() + ; + +TernaryExpressionSuffix + : TernaryExpressionOperator() (Type() | Variable()) + ::T_QMARK:: Type() @error("Syntax error, unexpected {token}, a condition must carry the type it is true of") + ::T_COLON:: @error("Syntax error, unexpected {token}, a condition must be parted with a colon \":\"") + Type() @error("Syntax error, unexpected {token}, a condition must carry the type it is false of") + ; + +// An "is" or an "is not" +TernaryExpressionOperator -> { + return \count($children) === 2; +} + : ? + ; + +/** + * ----------------------------------------------------------------------------- + * Logical Statements + * ----------------------------------------------------------------------------- + */ + +LogicalTail + : UnaryType() @error("Syntax error, unexpected {token}, a union type must carry a type after the vertical bar \"|\", for example \"T|U\"") + | UnaryType() @error("Syntax error, unexpected {token}, an intersection type must carry a type after the ampersand \"&\", for example \"T&U\"") + ; + +/** + * ----------------------------------------------------------------------------- + * Unary Statements + * ----------------------------------------------------------------------------- + */ + +UnaryType + : NullableType() + | ListOrOffsetType() + ; + +NullableType -> { + return new Type\NullableTypeNode($children[0], $offset); +} + : ::T_QMARK:: ListOrOffsetType() @error("Syntax error, unexpected {token}, a nullable type must carry the type it makes nullable, for example \"?T\"") + ; + +ListOrOffsetType -> { + $statement = $children[0]; + $count = \count($children); + + for ($i = 1; $i < $count; ++$i) { + $child = $children[$i]; + + switch (true) { + case $child === true: + if ($this->features->lists === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('square bracket list types', $source, $offset); + } + + $statement = new Type\TypesListNode($statement, $offset); + break; + case $child instanceof Type\TypeNode: + if ($this->features->offsets === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('type offsets', $source, $offset); + } + + $statement = new Type\TypeOffsetAccessNode($statement, $child, $offset); + break; + default: + throw Exception\InternalSemanticException::becauseSubNodeIsUnexpected( + \get_debug_type($child), + $source, + $offset, + ); + } + } + + return $statement; +} + : PrimaryType() + ListOrOffsetSuffix()* + ; + +ListOrOffsetSuffix -> { + return $children[0] ?? true; +} + : ::T_SQUARE_BRACKET_OPEN:: + Type()? + ::T_SQUARE_BRACKET_CLOSE:: @error("Syntax error, unexpected {token}, an offset must be closed with a bracket \"]\"") + ; + +PrimaryType + : ::T_PARENTHESIS_OPEN:: Type() ::T_PARENTHESIS_CLOSE:: @error("Syntax error, unexpected {token}, a group must be closed with a bracket \")\"") + | ThisType() + | LiteralType() + | NamedType() + | LeadingMaskType() + ; diff --git a/libs/parser/resources/grammar/attribute.pp2 b/libs/parser/resources/grammar/attribute.pp2 deleted file mode 100644 index 51319c6..0000000 --- a/libs/parser/resources/grammar/attribute.pp2 +++ /dev/null @@ -1,44 +0,0 @@ - -AttributeGroupsList -> { - return new Type\Attribute\AttributeGroupListNode($children); -} - : AttributeGroup()+ - ; - -AttributeGroup -> { - return new Type\Attribute\AttributeGroupNode($children); -} - : ::T_ATTR_OPEN:: - AttributesList() ::T_COMMA::? - ::T_SQUARE_BRACKET_CLOSE:: - ; - -AttributesList - : Attribute() ( - ::T_COMMA:: Attribute() - )* - ; - -Attribute -> { - return new Type\Attribute\AttributeNode( - $children[0], - ); -} - : Name() AttributeArguments()? - ; - -AttributeArguments -> { - return new Type\Attribute\AttributeArgumentListNode($children); -} - : ::T_PARENTHESIS_OPEN:: - AttributeArgument() ( - ::T_COMMA:: AttributeArgument() - )* ::T_COMMA::? - ::T_PARENTHESIS_CLOSE:: - ; - -AttributeArgument -> { - return new Type\Attribute\AttributeArgumentNode($children[0]); -} - : Type() - ; diff --git a/libs/parser/resources/grammar/callable.pp2 b/libs/parser/resources/grammar/callable.pp2 deleted file mode 100644 index b33a316..0000000 --- a/libs/parser/resources/grammar/callable.pp2 +++ /dev/null @@ -1,189 +0,0 @@ - -CallableType -> { - $name = \array_shift($children); - - if ($this->features->callables === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $offset); - } - - $parameters = isset($children[0]) && $children[0] instanceof Type\Callable\CallableParameterListNode - ? \array_shift($children) - : new Type\Callable\CallableParameterListNode(); - - return new Type\CallableTypeNode( - name: $name, - parameters: $parameters, - type: $children[0] ?? null, - ); -} - : Name() - ::T_PARENTHESIS_OPEN:: - CallableParameters()? - ::T_PARENTHESIS_CLOSE:: - CallableReturnType()? - ; - -CallableParameters -> { - return new Type\Callable\CallableParameterListNode($children); -} - : CallableParameter() - ( ::T_COMMA:: CallableParameter() )* - ::T_COMMA::? - ; - -CallableParameter -> { - $result = \end($children); - - if ($children[0] instanceof Type\Attribute\AttributeGroupListNode) { - if ($this->features->attributes === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable parameter attributes', $offset); - } - - $result->attributes = $children[0]; - } - - return $result; -} - : AttributeGroupsList()? - MaybeDefaultCallableParameter() - ; - -// Expects expression: -// - "" -// - " =" -MaybeDefaultCallableParameter -> { - if (\count($children) === 1) { - return $children[0]; - } - - if ($children[0]->isVariadic) { - throw Exception\VariadicWithDefaultException::becauseVariadicHasDefault($offset); - } - - $children[0]->isOptional = true; - return $children[0]; -} - : ( MaybePrefixedVariadicTypedNamedCallableParameter() - | MaybeModifiersNamedCallableParameter() ) - ? - ; - -// Expects expression: -// - "&..." -// - "...&" -// - "..." -// - "&" -// - "" -MaybeModifiersNamedCallableParameter -> { - if (!\is_array($children)) { - return $children; - } - - $result = \end($children); - - foreach ($children as $modifier) { - if ($modifier instanceof \Phplrt\Contracts\Lexer\TokenInterface) { - switch ($modifier->getName()) { - case 'T_AMP': - $result->isOutput = true; - break; - case 'T_ELLIPSIS': - if ($result->isVariadic) { - throw Exception\VariadicRedefinitionException::becauseVariadicIsRedefined($offset); - } - $result->isVariadic = true; - break; - } - } - } - - return $result; -} - : MaybeNamedCallableParameter() - | MaybeNamedCallableParameter() - | MaybeNamedCallableParameter() - | MaybeNamedCallableParameter() - | MaybeNamedCallableParameter() - ; - -// Expects expression: -// - "$" -MaybeNamedCallableParameter -> { - return new Type\Callable\CallableParameterNode(null, $children[0]); -} - : VariableLiteral() - ; - -MaybePrefixedVariadicTypedNamedCallableParameter -> { - if (\count($children) === 1) { - return $children[0]; - } - - if ($children[1]->isVariadic) { - throw Exception\VariadicRedefinitionException::becauseVariadicIsRedefined($offset); - } - - $children[1]->isVariadic = true; - return $children[1]; -} - : ? MaybeTypedNamedCallableParameter() - ; - -// Expects expression: -// - " $" -// - "" -MaybeTypedNamedCallableParameter -> { - if (\count($children) === 1) { - return $children[0]; - } - - $children[0]->name = $children[1]; - return $children[0]; -} - : MaybeModifiersTypedCallableParameter() - VariableLiteral()? - ; - -// Expects expression: -// - "...&" -// - "&..." -// - "&" -// - "..." -// - "" -MaybeModifiersTypedCallableParameter -> { - $result = \reset($children); - - foreach ($children as $modifier) { - if ($modifier instanceof \Phplrt\Contracts\Lexer\TokenInterface) { - switch ($modifier->getName()) { - case 'T_AMP': - $result->isOutput = true; - break; - case 'T_ELLIPSIS': - if ($result->isVariadic) { - throw Exception\VariadicRedefinitionException::becauseVariadicIsRedefined($offset); - } - $result->isVariadic = true; - break; - } - } - } - - return $result; -} - : TypedCallableParameter() - ( ? - | ? )? - ; - -// Expects expression: -// - "" -TypedCallableParameter -> { - return new Type\Callable\CallableParameterNode($children[0]); -} - : Type() - ; - -CallableReturnType - : ::T_COLON:: Type() - ; diff --git a/libs/parser/resources/grammar/callable.pp3 b/libs/parser/resources/grammar/callable.pp3 new file mode 100644 index 0000000..7326ab5 --- /dev/null +++ b/libs/parser/resources/grammar/callable.pp3 @@ -0,0 +1,67 @@ + +// The "(T, U): V" of a callable type +CallableSuffix + : ::T_PARENTHESIS_OPEN:: + CallableParameters() + ::T_PARENTHESIS_CLOSE:: @error("Syntax error, unexpected {token}, a parameter list must be closed with a bracket \")\"") + CallableReturnType()? + ; + +CallableParameters -> { + return new Type\Callable\CallableParameterListNode($children, $offset); +} + : ( CallableParameter() + ( ::T_COMMA:: CallableParameter() )* + ::T_COMMA::? + )? + ; + +CallableParameter -> { + $parameter = new Type\Callable\CallableParameterNode( + $children[0], + null, + false, + false, + false, + $offset, + ); + + for ($i = 1, $count = \count($children); $i < $count; ++$i) { + $child = $children[$i]; + + if ($child instanceof Type\VariableNode) { + $parameter->name = $child; + + continue; + } + + switch ($child->id) { + case self::T_AMP: + $parameter->isOutput = true; + break; + + case self::T_ELLIPSIS: + $parameter->isVariadic = true; + break; + + default: + if ($parameter->isVariadic) { + throw Exception\VariadicWithDefaultException::becauseVariadicHasDefault($source, $offset); + } + + $parameter->isOptional = true; + } + } + + return $parameter; +} + : Type() + ? + ? + Variable()? + ? + ; + +CallableReturnType + : ::T_COLON:: Type() @error("Syntax error, unexpected {token}, a callable must carry a type after the colon \":\"") + ; diff --git a/libs/parser/resources/grammar/common.pp2 b/libs/parser/resources/grammar/common.pp2 deleted file mode 100644 index 8662158..0000000 --- a/libs/parser/resources/grammar/common.pp2 +++ /dev/null @@ -1,33 +0,0 @@ - -Name - : FullQualifiedName() - | RelativeName() - ; - -FullQualifiedName -> { - return new Type\Name($children, true); -} - : ::T_NS_DELIMITER:: Identifier() (::T_NS_DELIMITER:: Identifier())* - ; - -RelativeName -> { - return new Type\Name($children, false); -} - : Identifier() (::T_NS_DELIMITER:: Identifier())* - ; - -Identifier -> { - return Type\Identifier::createFromString($children->getValue()); -} - : - | - | - | - | - ; - -IdentifierWithExtraSpace -> { - return Type\Identifier::createFromString($children->getValue()); -} - : - ; diff --git a/libs/parser/resources/grammar/common.pp3 b/libs/parser/resources/grammar/common.pp3 new file mode 100644 index 0000000..657138a --- /dev/null +++ b/libs/parser/resources/grammar/common.pp3 @@ -0,0 +1,68 @@ + +// "Some\Any" or "\Some\Any" +Name -> { + // "Some\Any" starts at a segment of its own + if ($children[0] instanceof Type\Identifier) { + return new Type\Name($children, false, $offset); + } + + // "\Some\Any" carries the separator in front of its segments, and the + // name itself is the part that follows it + $segments = \array_slice($children, 1); + + return new Type\Name($segments, true, $segments[0]->offset); +} + : ? Identifier() NameTail()* + ; + +NameTail + : ::T_NS_DELIMITER:: Identifier() @error("Syntax error, unexpected {token}, a name must carry a segment after the separator, for example \"Some\Any\"") + ; + +Identifier -> { + return new Type\Identifier( + $children->id === self::T_NAME_WITH_SPACE + ? \rtrim($children->value) + : $children->value, + $offset, + ); +} + : + | + | + | + | + | + | + ; + +IdentifierWithExtraSpace -> { + return new Type\Identifier(\rtrim($children->value), $offset); +} + : + ; + +// Any "$variable" (including "$this") +Variable -> { + return new Type\VariableNode( + new Type\Identifier(\substr($children->value, 1), $offset), + $offset, + ); +} + : + | + ; + +// The "$this" of a "callable(): $this" +ThisType -> { + return new Type\ThisNode($offset); +} + : + ; + +// "*" in "T<*>" or "JSON_*" +WildcardType -> { + return new Type\WildcardNode($offset); +} + : + ; diff --git a/libs/parser/resources/grammar/lexemes.pp2 b/libs/parser/resources/grammar/lexemes.pp2 deleted file mode 100644 index 4edb9e5..0000000 --- a/libs/parser/resources/grammar/lexemes.pp2 +++ /dev/null @@ -1,53 +0,0 @@ - -// Literals - -%token T_DQ_STRING_LITERAL "([^"\\]*(?:\\.[^"\\]*)*)" -%token T_SQ_STRING_LITERAL '([^'\\]*(?:\\.[^'\\]*)*)' -%token T_PFX_FLOAT_LITERAL \-?[0-9]++\.[0-9]*+(?:[eE]-?[0-9]++)? -%token T_SFX_FLOAT_LITERAL \-?[0-9]*+\.[0-9]++(?:[eE]-?[0-9]++)? -%token T_EXP_LITERAL \-?[0-9]++[eE]-?[0-9]++ -%token T_BIN_INT_LITERAL \-?0[bB][01_]++ -%token T_OCT_INT_LITERAL \-?0[oO][0-7_]++ -%token T_HEX_INT_LITERAL \-?0[xX][0-9a-fA-F_]++ -%token T_DEC_INT_LITERAL \-?[0-9][0-9_]*+ -%token T_BOOL_LITERAL (?i)(?:true|false)(?![a-zA-Z0-9\-_\x80-\xff]) -%token T_NULL_LITERAL (?i)null(?![a-zA-Z0-9\-_\x80-\xff]) - -// Identifier - -%token T_NEQ (?i)is\h++not(?![a-zA-Z0-9\-_\x80-\xff]) -%token T_EQ (?i)is(?![a-zA-Z0-9\-_\x80-\xff]) -%token T_THIS \$this\b -%token T_VARIABLE \$[a-zA-Z_\x80-\xff][a-zA-Z0-9\-_\x80-\xff]*+ -%token T_NAME_WITH_SPACE [a-zA-Z_\x80-\xff][a-zA-Z0-9\-_\x80-\xff]*+\s++ -%token T_NAME [a-zA-Z_\x80-\xff][a-zA-Z0-9\-_\x80-\xff]*+ - -// Special Chars - -%token T_LTE <= -%token T_GTE >= -%token T_ANGLE_BRACKET_OPEN < -%token T_ANGLE_BRACKET_CLOSE > -%token T_PARENTHESIS_OPEN \( -%token T_PARENTHESIS_CLOSE \) -%token T_BRACE_OPEN \{ -%token T_BRACE_CLOSE \} -%token T_ATTR_OPEN #\[ -%token T_SQUARE_BRACKET_OPEN \[ -%token T_SQUARE_BRACKET_CLOSE \] -%token T_COMMA , -%token T_ELLIPSIS \.\.\. -%token T_DOUBLE_COLON :: -%token T_COLON : -%token T_ASSIGN = -%token T_NS_DELIMITER \\ -%token T_QMARK \? -%token T_OR \| -%token T_AMP & -%token T_ASTERISK \* - -// Other - -%skip T_COMMENT (?://|#)[^\r\n]*+ -%skip T_DOC_COMMENT /\*.*?\*/ -%skip T_WHITESPACE \s++ diff --git a/libs/parser/resources/grammar/lexemes.pp3 b/libs/parser/resources/grammar/lexemes.pp3 new file mode 100644 index 0000000..d8351c8 --- /dev/null +++ b/libs/parser/resources/grammar/lexemes.pp3 @@ -0,0 +1,126 @@ + +/** + * ----------------------------------------------------------------------------- + * Pattern Fragments + * ----------------------------------------------------------------------------- + */ + +// +// Identifiers +// + +%fragment IDENT_HEAD [a-zA-Z_\x80-\xff] +%fragment IDENT_TAIL [a-zA-Z0-9\-_\x80-\xff] + +// The "foo_1" of a "$foo_1", which carries no dash +%fragment VAR_TAIL [a-zA-Z0-9_\x80-\xff] + +%fragment KEYWORD_END (?!(?&IDENT_TAIL)) + +// +// Numbers +// + +// The "-" of a "-42" +%fragment SIGN [-+] + +// The "F" of a "0xFF" +%fragment DIGIT_HEAD [0-9] +%fragment BIN_HEAD [01] +%fragment OCT_HEAD [0-7] +%fragment HEX_HEAD [0-9a-fA-F] + +%fragment DEC_HEAD [1-9] + +%fragment DIGIT_TAIL _?(?&DIGIT_HEAD) +%fragment DEC_TAIL _?(?&DIGIT_HEAD) +%fragment BIN_TAIL _?(?&BIN_HEAD) +%fragment OCT_TAIL _?(?&OCT_HEAD) +%fragment HEX_TAIL _?(?&HEX_HEAD) + +// The "FE_DE" of a "0xFE_DE" +%fragment DIGITS (?&DIGIT_HEAD)(?&DIGIT_TAIL)*+ +%fragment DEC_DIGITS (?&DEC_HEAD)(?&DEC_TAIL)*+ +%fragment BIN_DIGITS (?&BIN_HEAD)(?&BIN_TAIL)*+ +%fragment OCT_DIGITS (?&OCT_HEAD)(?&OCT_TAIL)*+ +%fragment HEX_DIGITS (?&HEX_HEAD)(?&HEX_TAIL)*+ + +// A "42.0", a "42." or a ".42" +%fragment FRACTION (?&DIGITS)\.(?:(?&DIGITS))?|\.(?&DIGITS) + +// An "e2", an "E-2" or an "e+2" +%fragment EXPONENT [eE](?&SIGN)?(?&DIGITS) + +/** + * ----------------------------------------------------------------------------- + * Lexeme/Token Definitions + * ----------------------------------------------------------------------------- + */ + +%skip T_WHITESPACE \s++ + +// An "is" +%token T_EQ is(?&KEYWORD_END) +// A "not" +%token T_NOT not(?&KEYWORD_END) +// A "true" or a "TRUE" +%token T_TRUE_LITERAL (?i:true)(?&KEYWORD_END) +// A "false" or a "FALSE" +%token T_FALSE_LITERAL (?i:false)(?&KEYWORD_END) +// A "null" or a "NULL" +%token T_NULL_LITERAL (?i:null)(?&KEYWORD_END) + +// Identifiers + +%token T_NAME_WITH_SPACE (?&IDENT_HEAD)(?&IDENT_TAIL)*+\s++ +// An "int", a "non-empty-string" or a "_42" +%token T_NAME (?&IDENT_HEAD)(?&IDENT_TAIL)*+ + +%token T_NS_DELIMITER \\ +%token T_OR \| +%token T_COMMA , +%token T_DOUBLE_COLON :: +%token T_COLON : +%token T_ANGLE_BRACKET_OPEN < +%token T_ANGLE_BRACKET_CLOSE > +%token T_QMARK \? +%token T_BRACE_OPEN \{ +%token T_BRACE_CLOSE \} +%token T_PARENTHESIS_OPEN \( +%token T_PARENTHESIS_CLOSE \) +%token T_SQUARE_BRACKET_OPEN \[ +%token T_SQUARE_BRACKET_CLOSE \] +%token T_AMP & +%token T_ELLIPSIS \.\.\. +%token T_ASSIGN = +%token T_ASTERISK \* + +// Variables + +// A "$this" +%token T_THIS \$this(?!(?&VAR_TAIL)) +// A "$value" or a "$_" +%token T_VARIABLE \$(?&IDENT_HEAD)(?&VAR_TAIL)*+ + +// Literals + +// A double quoted "text" +%token T_DQ_STRING_LITERAL "[^"\\]*+(?:\\.[^"\\]*+)*+" +%token T_SQ_STRING_LITERAL '[^'\\]*+(?:\\.[^'\\]*+)*+' +// A "42.0", a "42.", a ".42", a "42e2", a "-1.5e+3" or a "2_3.4_5e-6_7" +%token T_FLOAT_LITERAL (?&SIGN)?(?:(?&FRACTION)(?:(?&EXPONENT))?|(?&DIGITS)(?&EXPONENT)) +// A "0b1010", a "0B0001_1000" or a "-0b10" +%token T_BIN_INT_LITERAL (?&SIGN)?0[bB](?&BIN_DIGITS) +// A "0o42", a "0O42", a "042" or an "04_23" +%token T_OCT_INT_LITERAL (?&SIGN)?0(?:[oO](?&OCT_DIGITS)|(?&OCT_TAIL)++) +// A "0xDEAD", a "0XDeaD" or a "0xFE_DE" +%token T_HEX_INT_LITERAL (?&SIGN)?0[xX](?&HEX_DIGITS) +// A "42", a "+42_04" or a "0" +%token T_DEC_INT_LITERAL (?&SIGN)?(?:(?&DEC_DIGITS)|0) + +// Comments + +// A "// text" or a "# text" +%skip T_COMMENT (?://|#)[^\r\n]*+ +// A "/* text */" +%skip T_DOC_COMMENT /\*.*?\*/ diff --git a/libs/parser/resources/grammar/literals.pp2 b/libs/parser/resources/grammar/literals.pp2 deleted file mode 100644 index 2a0e750..0000000 --- a/libs/parser/resources/grammar/literals.pp2 +++ /dev/null @@ -1,111 +0,0 @@ - -Literal - : RawLiteral() - | ConstMaskLiteral() - | ClassConstLiteral() - ; - -RawLiteral -> { - if ($this->features->literals === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('literal values', $offset); - } - return $children; -} - : StringLiteral() - | FloatLiteral() - | IntLiteral() - | BoolLiteral() - | NullLiteral() - ; - -VariableLiteral -> { - return Type\Literal\VariableLiteralNode::parse($token->getValue()); -} - : - | - ; - -ThisLiteral -> { - return Type\Literal\VariableLiteralNode::parse($token->getValue()); -} - : - ; - -StringLiteral -> { return $this->stringPool[$token] ??= $children; } - : DoubleQuotedStringLiteral() - | SingleQuotedStringLiteral() - ; - -DoubleQuotedStringLiteral -> { - return Type\Literal\StringLiteralNode::createFromDoubleQuotedString($token->getValue()); -} - : - ; - -SingleQuotedStringLiteral -> { - return Type\Literal\StringLiteralNode::createFromSingleQuotedString($token->getValue()); -} - : - ; - -FloatLiteral -> { - return Type\Literal\FloatLiteralNode::parse($token->getValue()); -} - : - | - | - ; - -IntLiteral -> { - return $this->integerPool[$token] ??= Type\Literal\IntLiteralNode::parse($token->getValue()); -} - : - | - | - | - ; - -BoolLiteral -> { - return Type\Literal\BoolLiteralNode::parse($token->getValue()); -} - : - ; - -NullLiteral -> { - return new Type\Literal\NullLiteralNode($children->getValue()); -} - : - ; - -ConstMaskLiteral -> { - return new Type\ConstMaskNode($children[0]); -} - : Name() ::T_ASTERISK:: - ; - -ClassConstLiteral -> { - // :: "*" - if (\count($children) === 3) { - return new Type\ClassConstMaskNode( - $children[0], - $children[1], - ); - } - - // :: - if ($children[1] instanceof Type\Identifier) { - return new Type\ClassConstNode( - $children[0], - $children[1], - ); - } - - // :: "*" - return new Type\ClassConstMaskNode($children[0]); -} - : Name() ::T_DOUBLE_COLON:: - ( Identifier() - | Identifier() - | - ) - ; diff --git a/libs/parser/resources/grammar/literals.pp3 b/libs/parser/resources/grammar/literals.pp3 new file mode 100644 index 0000000..dea24e6 --- /dev/null +++ b/libs/parser/resources/grammar/literals.pp3 @@ -0,0 +1,90 @@ + +LiteralType -> { + if ($this->features->literals === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('literal values', $source, $offset); + } + return $children; +} + : ScalarType() + | NullLiteralType() + ; + +ScalarType + : StringLiteralType() + | FloatLiteralType() + | IntLiteralType() + | TrueLiteralType() + | FalseLiteralType() + ; + +StringLiteralType + : DoubleQuotedStringLiteralType() + | SingleQuotedStringLiteralType() + ; + +DoubleQuotedStringLiteralType -> { + return new Type\Literal\StringLiteralNode( + StringDecoder::unpackAndDecode($children->value, true), + $children->value, + $offset, + ); +} + : + ; + +SingleQuotedStringLiteralType -> { + return new Type\Literal\StringLiteralNode( + StringDecoder::unpackAndDecode($children->value, false), + $children->value, + $offset, + ); +} + : + ; + +FloatLiteralType -> { + return new Type\Literal\FloatLiteralNode( + FloatDecoder::decode($children->value), + $children->value, + $offset, + ); +} + : + ; + +IntLiteralType -> { + $decoded = IntDecoder::decode($children->value); + + return new Type\Literal\IntLiteralNode( + $decoded->value, + $children->value, + $decoded->decimal, + $offset, + ); +} + : + | + | + | + ; + +TrueLiteralType -> { + return new Type\Literal\BoolLiteralNode(true, $children->value, $offset); +} + : + ; + +FalseLiteralType -> { + return new Type\Literal\BoolLiteralNode(false, $children->value, $offset); +} + : + ; + +NullLiteralType -> { + return new Type\Literal\NullLiteralNode( + $children->value, + $offset, + ); +} + : + ; diff --git a/libs/parser/resources/grammar/named-type.pp2 b/libs/parser/resources/grammar/named-type.pp2 deleted file mode 100644 index 300654d..0000000 --- a/libs/parser/resources/grammar/named-type.pp2 +++ /dev/null @@ -1,22 +0,0 @@ - -NamedType -> { - $fields = $parameters = null; - - // Shape fields - if (\end($children) instanceof Type\Shape\FieldsListNode) { - $fields = \array_pop($children); - } - - // Template parameters - if (\end($children) instanceof Type\Template\TemplateArgumentListNode) { - $parameters = \array_pop($children); - } - - return new Type\NamedTypeNode( - $children[0], - $parameters, - $fields, - ); -} - : Name() (TemplateArguments() | ShapeFields())? - ; diff --git a/libs/parser/resources/grammar/named-type.pp3 b/libs/parser/resources/grammar/named-type.pp3 new file mode 100644 index 0000000..a76e73e --- /dev/null +++ b/libs/parser/resources/grammar/named-type.pp3 @@ -0,0 +1,118 @@ + +/** + * ----------------------------------------------------------------------------- + * Named Types + * ----------------------------------------------------------------------------- + */ + +// A name is read by this very rule rather than by one of its own: a type is +// named far more often than anything else is read, so every rule the reading +// goes through on its way here is paid for by the whole grammar. +NamedType -> { + $head = $children[0]; + $suffix = $children[1] ?? null; + + // "Some\Any\*" and "Some\Any\SOME_*" + // + // A mask closes the type it is written at the end of, so it is read as a + // suffix of a name and never carries a suffix of its own. + if ($suffix instanceof Type\MaskNode || $suffix instanceof Type\WildcardNode) { + $parts = $head->parts; + + if ($suffix instanceof Type\MaskNode) { + // "Some\Any\*" + $mask = $suffix; + } else { + // "Some\Any\SOME_*" + $segment = \array_pop($parts); + $mask = new Type\MaskNode([$segment, ...\array_slice($children, 1)], $segment->offset); + } + + return new Type\ConstMaskNode( + $mask, + $parts === [] + ? $head->isFullyQualified + : new Type\Name($parts, $head->isFullyQualified, $head->offset), + $offset, + ); + } + + switch (true) { + // Some\Any + case $suffix === null: + return new Type\NamedTypeNode($children[0], null, null, $offset); + + // Some\Any + case $suffix instanceof Type\Template\TemplateArgumentListNode: + return new Type\NamedTypeNode($children[0], $suffix, null, $offset); + + // Some\Any{name: T, ...} + case $suffix instanceof Type\Shape\FieldsListNode: + return new Type\NamedTypeNode($children[0], $children[2] ?? null, $suffix, $offset); + + // Some\Any(T, U): V + case $suffix instanceof Type\Callable\CallableParameterListNode: + if ($this->features->callables === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $source, $offset); + } + + return new Type\CallableTypeNode($children[0], $suffix, $children[2] ?? null, null, $offset); + + // Some\Any(T): U + case $suffix instanceof Type\Template\TemplateParameterListNode: + if ($this->features->callables === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $source, $offset); + } + + return new Type\CallableTypeNode($children[0], $children[2], $children[3] ?? null, $suffix, $offset); + + // Some\Any::CONST, Some\Any::CONST_* and Some\Any::* + default: + if (\count($children) === 3 && $children[2] instanceof Type\Identifier) { + return new Type\ClassConstNode($children[0], $children[2], $offset); + } + + return new Type\ClassConstMaskNode( + $children[0], + new Type\MaskNode(\array_slice($children, 2), $children[2]->offset), + $offset, + ); + } +} + : Name() NamedTypeSuffix()? + ; + +NamedTypeSuffix + : ClassConstSuffix() + | CallableSuffix() + | TemplateArguments() !::T_PARENTHESIS_OPEN:: + | TemplateParameters() CallableSuffix() + | ShapeFields() + | MaskTail() + | NamespacedMask() + ; + +// "*_SOME" — a mask that carries no name in front of it +LeadingMaskType -> { + return new Type\ConstMaskNode(new Type\MaskNode($children, $offset), false, $offset); +} + : WildcardType() Identifier() MaskTail()? + ; + +MaskTail + : WildcardType() ( Identifier() WildcardType() )* Identifier()? + ; + +NamespacedMask -> { + return new Type\MaskNode($children, $offset); +} + : ::T_NS_DELIMITER:: MaskTail() + ; + +// The "::CONST", "::CONST_*", "::*" and "::A*B" of a class constant +ClassConstSuffix + : + ( Identifier() MaskTail()? + | MaskTail() + ) @error("Syntax error, unexpected {token}, a class constant must carry a name after the double colon, for example \"Some::CONST\"") + ; diff --git a/libs/parser/resources/grammar/shape-fields.pp2 b/libs/parser/resources/grammar/shape-fields.pp2 deleted file mode 100644 index 0c1a2fe..0000000 --- a/libs/parser/resources/grammar/shape-fields.pp2 +++ /dev/null @@ -1,128 +0,0 @@ - -ShapeFields -> { - if ($children === []) { - return new Type\Shape\FieldsListNode(); - } - - if ($this->features->shapes === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape fields', $offset); - } - - $parameters = null; - - if (\end($children) instanceof Type\Template\TemplateArgumentListNode) { - $parameters = \array_pop($children); - } - - $fields = \reset($children) instanceof Type\Shape\FieldsListNode - ? \array_shift($children) - : new Type\Shape\FieldsListNode(); - - if ($children !== []) { - $fields->sealed = false; - } - - return \array_filter([$parameters, $fields]); -} - : ::T_BRACE_OPEN:: - ( - (ShapeFieldsList() (::T_COMMA:: ShapeFieldsUnsealed())?) - | ShapeFieldsUnsealed()? - ) - ::T_COMMA::? - ::T_BRACE_CLOSE:: - ; - -ShapeFieldsUnsealed - : TemplateArguments()? - ; - -ShapeFieldsList -> { - $explicit = []; - $implicit = false; - - foreach ($children as $field) { - if ($field instanceof Type\Shape\ExplicitFieldNode) { - $key = $field->index; - - if (\in_array($key, $explicit, true)) { - throw Exception\ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated($key, $field->offset); - } - - $explicit[] = $key; - } else { - $implicit = true; - } - } - - if ($explicit !== [] && $implicit) { - throw Exception\ShapeKeysMixingException::becauseShapeKeysAreMixed($offset); - } - - return new Type\Shape\FieldsListNode($children); -} - : ShapeField() ( - ::T_COMMA:: ShapeField() - )* - ; - -ShapeField -> { - $result = \end($children); - - if ($children[0] instanceof Type\Attribute\AttributeGroupListNode) { - if ($this->features->attributes === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape field attributes', $offset); - } - - $result->attributes = $children[0]; - } - - return $result; -} - : AttributeGroupsList()? ( - ExplicitField() - | ImplicitField() - ) - ; - -ExplicitField -> { - $name = $children[0]; - $value = \array_pop($children); - - // In case of "nullable" suffix defined - $optional = \count($children) === 2; - - return match (true) { - $name instanceof Type\Literal\IntLiteralNode - => new Type\Shape\NumericFieldNode($name, $value, $optional), - $name instanceof Type\Literal\StringLiteralNode - => new Type\Shape\StringNamedFieldNode($name, $value, $optional), - $name instanceof Type\ClassConstNode - => new Type\Shape\ClassConstFieldNode($name, $value, $optional), - $name instanceof Type\ClassConstMaskNode - => new Type\Shape\ClassConstMaskFieldNode($name, $value, $optional), - $name instanceof Type\ConstMaskNode - => new Type\Shape\ConstMaskFieldNode($name, $value, $optional), - default => new Type\Shape\NamedFieldNode($name, $value, $optional), - }; -} - : ShapeKey() ()? ::T_COLON:: ShapeValue() - ; - -ImplicitField -> { - return new Type\Shape\ImplicitFieldNode($children[0]); -} - : ShapeValue() - ; - -ShapeKey - : ConstMaskLiteral() - | ClassConstLiteral() - | Identifier() - | IntLiteral() - | StringLiteral() - ; - -ShapeValue - : Type() - ; diff --git a/libs/parser/resources/grammar/shape-fields.pp3 b/libs/parser/resources/grammar/shape-fields.pp3 new file mode 100644 index 0000000..bdd0479 --- /dev/null +++ b/libs/parser/resources/grammar/shape-fields.pp3 @@ -0,0 +1,138 @@ + +ShapeFields -> { + $count = \count($children); + + if ($count === 0) { + return [new Type\Shape\FieldsListNode([], true, $offset)]; + } + + if ($this->features->shapes === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape fields', $source, $offset); + } + + $parameters = null; + + // The "..." suffix + if ($children[$count - 1] instanceof Type\Template\TemplateArgumentListNode) { + $parameters = $children[$count - 1]; + --$count; + } + + if ($children[0] instanceof Type\Shape\FieldsListNode) { + $fields = $children[0]; + $unsealed = $count > 1; + } else { + $fields = new Type\Shape\FieldsListNode([], true, $offset); + $unsealed = $count > 0; + } + + if ($unsealed) { + $fields->isSealed = false; + } + + if ($parameters === null) { + return [$fields]; + } + + return [$fields, $parameters]; +} + : ::T_BRACE_OPEN:: + ( + (ShapeFieldsList() (::T_COMMA:: ShapeFieldsUnsealed())?) + | ShapeFieldsUnsealed()? + ) + ::T_COMMA::? + ::T_BRACE_CLOSE:: @error("Syntax error, unexpected {token}, a shape must be closed with a brace \"}\"") + ; + +ShapeFieldsUnsealed + : TemplateArguments()? + ; + +ShapeFieldsList -> { + $explicit = false; + $implicit = false; + $indexed = []; + + foreach ($children as $field) { + if (!$field instanceof Type\Shape\ExplicitFieldNode) { + $implicit = true; + + continue; + } + + $explicit = true; + + if (!$field instanceof Type\Shape\SimpleFieldNodeInterface) { + continue; + } + + $key = $field->getIndex(); + + if (isset($indexed[$key])) { + throw Exception\ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated($key, $source, $field->offset); + } + + $indexed[$key] = true; + } + + if ($explicit && $implicit) { + throw Exception\ShapeKeysMixingException::becauseShapeKeysAreMixed($source, $offset); + } + + return new Type\Shape\FieldsListNode($children, true, $offset); +} + : ShapeField() ( + ::T_COMMA:: ShapeField() + )* + ; + +ShapeField + : ExplicitField() + | ImplicitField() + ; + +ExplicitField -> { + $count = \count($children); + $name = $children[0]; + $value = $children[$count - 1]; + + $optional = $count === 3; + + return match (true) { + // "a", "true" and "null" + $name instanceof Type\Identifier + => new Type\Shape\NamedFieldNode($name, $value, $optional, $offset), + $name instanceof Type\Literal\IntLiteralNode, + $name instanceof Type\Literal\StringLiteralNode + => new Type\Shape\ScalarFieldNode($name, $value, $optional, $offset), + // "Some::CONST", "Some::CONST_*" and "JSON_*" + $name instanceof Type\ClassConstNode, + $name instanceof Type\ClassConstMaskNode, + $name instanceof Type\ConstMaskNode + => new Type\Shape\ComplexFieldNode($name, $value, $optional, $offset), + default => throw Exception\ShapeKeyException::becauseKeyIsNotAName($source, $name->offset), + }; +} + : ShapeKey() ()? ::T_COLON:: ShapeValue() @error("Syntax error, unexpected {token}, a shape field must carry a type after the colon \":\"") + ; + +ImplicitField -> { + return new Type\Shape\ImplicitFieldNode($children[0], false, $offset); +} + : ShapeValue() + ; + +ShapeKey + : Identifier() &ShapeKeyEnd() + | PrimaryType() + ; + +// The "?:" and ":" a shape key is followed by +ShapeKeyEnd + : ::T_QMARK::? ::T_COLON:: + ; + +ShapeValue + : Type() + ; diff --git a/libs/parser/resources/grammar/template-arguments.pp2 b/libs/parser/resources/grammar/template-arguments.pp2 deleted file mode 100644 index e0cecce..0000000 --- a/libs/parser/resources/grammar/template-arguments.pp2 +++ /dev/null @@ -1,55 +0,0 @@ - -TemplateArguments -> { - if ($this->features->generics === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template arguments', $offset); - } - - return new Type\Template\TemplateArgumentListNode($children); -} - : ::T_ANGLE_BRACKET_OPEN:: - TemplateArgument() ( - ::T_COMMA:: TemplateArgument() - )* ::T_COMMA::? - ::T_ANGLE_BRACKET_CLOSE:: - ; - -TemplateArgument -> { - $hint = $attributes = null; - - if (\reset($children) instanceof Type\Attribute\AttributeGroupListNode) { - if ($this->features->attributes === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template argument attributes', $offset); - } - - $attributes = \array_shift($children); - } - - $type = \array_pop($children); - - if (\reset($children) !== false) { - if ($this->features->hints === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template argument hints', $offset); - } - - $hint = \reset($children); - } - - return new Type\Template\TemplateArgumentNode( - $type, - $hint, - $attributes, - ); -} - : AttributeGroupsList()? ( - TemplateHintedArgument() - | TemplateSimpleArgument() - ) - ; - -TemplateSimpleArgument - : Type() - ; - -TemplateHintedArgument - : IdentifierWithExtraSpace() Type() - ; diff --git a/libs/parser/resources/grammar/template-arguments.pp3 b/libs/parser/resources/grammar/template-arguments.pp3 new file mode 100644 index 0000000..c8b8e3d --- /dev/null +++ b/libs/parser/resources/grammar/template-arguments.pp3 @@ -0,0 +1,43 @@ +/** + * ----------------------------------------------------------------------------- + * Template Arguments + * ----------------------------------------------------------------------------- + */ + +TemplateArguments -> { + if ($this->features->generics === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template arguments', $source, $offset); + } + + return new Type\Template\TemplateArgumentListNode($children, $offset); +} + : ::T_ANGLE_BRACKET_OPEN:: + TemplateArgument() @error("Syntax error, unexpected {token}, an argument list must carry at least one argument") ( + ::T_COMMA:: TemplateArgument() + )* ::T_COMMA::? + ::T_ANGLE_BRACKET_CLOSE:: @error("Syntax error, unexpected {token}, an argument list must be closed with a bracket \">\"") + ; + +TemplateArgument -> { + if (\is_array($children)) { + if ($this->features->hints === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template argument hints', $source, $offset); + } + + return new Type\Template\TemplateArgumentNode($children[1], $children[0], $offset); + } + + return new Type\Template\TemplateArgumentNode($children, null, $offset); +} + : TemplateHintedArgument() + | TemplateSimpleArgument() + ; + +TemplateSimpleArgument + : WildcardType() + | Type() + ; + +TemplateHintedArgument + : IdentifierWithExtraSpace() TemplateSimpleArgument() + ; diff --git a/libs/parser/resources/grammar/template-parameters.pp3 b/libs/parser/resources/grammar/template-parameters.pp3 new file mode 100644 index 0000000..7d316cf --- /dev/null +++ b/libs/parser/resources/grammar/template-parameters.pp3 @@ -0,0 +1,79 @@ +/** + * ----------------------------------------------------------------------------- + * Template Parameters + * ----------------------------------------------------------------------------- + */ + +TemplateParameters -> { + if ($this->features->generics === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template parameters', $source, $offset); + } + + return new Type\Template\TemplateParameterListNode($children, $offset); +} + : ::T_ANGLE_BRACKET_OPEN:: + TemplateParameter() @error("Syntax error, unexpected {token}, a parameter list must carry at least one parameter") ( + ::T_COMMA:: TemplateParameter() + )* ::T_COMMA::? + ::T_ANGLE_BRACKET_CLOSE:: @error("Syntax error, unexpected {token}, a parameter list must be closed with a bracket \">\"") + ; + +TemplateParameter -> { + $upper = $lower = $default = null; + + foreach (\array_slice($children, 1) as $limit) { + if (!$limit instanceof Type\Template\TemplateBoundEdgeNode) { + if ($default !== null) { + throw Exception\TemplateBoundException::becauseBoundIsDuplicated('default', $source, $limit->offset); + } + + $default = $limit; + + continue; + } + + if ($default !== null) { + throw Exception\TemplateBoundException::becauseDefaultIsNotWrittenLast($source, $limit->operator->offset); + } + + switch ($limit->operator->value) { + case 'of': + case 'as': + if ($upper !== null) { + throw Exception\TemplateBoundException::becauseBoundIsDuplicated('upper bound', $source, $limit->offset); + } + + $upper = $limit; + break; + + case 'super': + if ($lower !== null) { + throw Exception\TemplateBoundException::becauseBoundIsDuplicated('lower bound', $source, $limit->offset); + } + + $lower = $limit; + break; + + default: + throw Exception\TemplateBoundException::becauseOperatorIsUnknown( + $limit->operator->value, + $source, + $limit->operator->offset, + ); + } + } + + return new Type\Template\TemplateParameterNode($children[0], $upper, $lower, $default, $offset); +} + : Identifier() ( TemplateBoundEdge() | TemplateDefault() )* + ; + +TemplateBoundEdge -> { + return new Type\Template\TemplateBoundEdgeNode($children[0], $children[1], $offset); +} + : Identifier() Type() + ; + +TemplateDefault + : ::T_ASSIGN:: Type() + ; diff --git a/libs/parser/resources/grammar/ternary.pp2 b/libs/parser/resources/grammar/ternary.pp2 deleted file mode 100644 index cd86fdc..0000000 --- a/libs/parser/resources/grammar/ternary.pp2 +++ /dev/null @@ -1,71 +0,0 @@ - -TernaryExpressionOrLogicalType -> { - $count = \count($children); - - if ($count === 1) { - return $children[0]; - } - - if ($this->features->conditions === false) { - throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('conditional expressions', $offset); - } - - $condition = match ($children[1]->getName()) { - 'T_EQ' => new Type\Condition\EqualConditionNode( - $children[0], - $children[2], - ), - 'T_NEQ' => new Type\Condition\NotEqualConditionNode( - $children[0], - $children[2], - ), - 'T_GTE' => new Type\Condition\GreaterThanOrEqualConditionNode( - $children[0], - $children[2], - ), - 'T_ANGLE_BRACKET_CLOSE' => new Type\Condition\GreaterThanConditionNode( - $children[0], - $children[2], - ), - 'T_LTE' => new Type\Condition\LessThanOrEqualConditionNode( - $children[0], - $children[2], - ), - 'T_ANGLE_BRACKET_OPEN' => new Type\Condition\LessThanConditionNode( - $children[0], - $children[2], - ), - default => throw Exception\InvalidConditionalOperatorException::becauseConditionalOperatorIsInvalid( - $children[1]->getValue(), - $offset, - ), - }; - - return new Type\TernaryExpressionNode( - $condition, - $children[3], - $children[4], - ); -} - : LogicalType() OptionalTernaryExpressionSuffix() - | VariableLiteral() TernaryExpressionSuffix() - ; - -OptionalTernaryExpressionSuffix - : TernaryExpressionSuffix()? - ; - -TernaryExpressionSuffix - : TernaryExpressionOperator() (Type() | VariableLiteral()) - ::T_QMARK:: Type() - ::T_COLON:: Type() - ; - -TernaryExpressionOperator - : - | - | - | - | - | - ; diff --git a/libs/parser/src/Exception/ConstMaskException.php b/libs/parser/src/Exception/ConstMaskException.php new file mode 100644 index 0000000..91e81db --- /dev/null +++ b/libs/parser/src/Exception/ConstMaskException.php @@ -0,0 +1,29 @@ + $offset + */ + public static function becauseNothingFollowsAMask( + ReadableInterface $source, + int $offset = 0, + ): self { + return new self( + self::describe( + 'Constant mask is a type entire and cannot be followed by anything', + $source, + ), + $source, + self::createToken($source, $offset), + ); + } +} diff --git a/libs/parser/src/Exception/FeatureNotAllowedException.php b/libs/parser/src/Exception/FeatureNotAllowedException.php index e39e51b..61d63e9 100644 --- a/libs/parser/src/Exception/FeatureNotAllowedException.php +++ b/libs/parser/src/Exception/FeatureNotAllowedException.php @@ -4,6 +4,8 @@ namespace TypeLang\Parser\Exception; +use Phplrt\Contracts\Source\ReadableInterface; + final class FeatureNotAllowedException extends SemanticException { /** @@ -13,10 +15,18 @@ final class FeatureNotAllowedException extends SemanticException * @param non-empty-string $name * @param int<0, max> $offset */ - public static function becauseFeatureIsNotAllowed(string $name, int $offset = 0): self - { - $message = \sprintf('%s not allowed', \ucfirst($name)); - - return new self($offset, $message); + public static function becauseFeatureIsNotAllowed( + string $name, + ReadableInterface $source, + int $offset = 0, + ): self { + return new self( + self::describe( + \sprintf('%s not allowed', \ucfirst($name)), + $source, + ), + $source, + self::createToken($source, $offset), + ); } } diff --git a/libs/parser/src/Exception/Formatter.php b/libs/parser/src/Exception/Formatter.php deleted file mode 100644 index 0a8539d..0000000 --- a/libs/parser/src/Exception/Formatter.php +++ /dev/null @@ -1,56 +0,0 @@ - 'end of input', - '"' => 'double quote (")', - default => \sprintf('"%s"', \addcslashes($expr, '"')), - }; - } - - public static function source(string $statement): string - { - $statement = \trim($statement); - - if ($statement === '') { - return ''; - } - - $renderer = new Renderer(); - - return $renderer->value(new Token('', $statement, 0)); - } - - /** - * @param int<0, max> $offset - * @return non-empty-string - */ - public static function suffix(string $statement, int $offset): string - { - if (\str_contains($statement, "\n")) { - $pos = Position::fromOffset($statement, $offset); - - return \sprintf('on line %d at column %d', $pos->getLine(), $pos->getColumn()); - } - - return \sprintf('at column %d', $offset + 1); - } -} diff --git a/libs/parser/src/Exception/InternalParseException.php b/libs/parser/src/Exception/InternalParseException.php index 06a388f..2b63d30 100644 --- a/libs/parser/src/Exception/InternalParseException.php +++ b/libs/parser/src/Exception/InternalParseException.php @@ -4,33 +4,17 @@ namespace TypeLang\Parser\Exception; -use Phplrt\Contracts\Source\SourceExceptionInterface; +use Phplrt\Contracts\Source\ReadableInterface; -final class InternalParseException extends ParseException +final class InternalParseException extends ParserException { /** * Occurs when an unexpected error is raised while parsing a statement. */ - public static function becauseInternalErrorOccurs(string $statement, \Throwable $e): self + public static function becauseInternalErrorOccurs(ReadableInterface $source, \Throwable $e): self { - $message = \sprintf('An internal error occurred while parsing %s', Formatter::source($statement)); + $message = \sprintf('An internal error occurred while parsing %s', self::printSource($source)); - return new self($message, self::ERROR_CODE_INTERNAL_ERROR, $e); - } - - /** - * Occurs when the parser produces no result for a readable statement. - */ - public static function becauseTypeStatementIsUnreadable(): self - { - return new self('Could not read type statement', self::ERROR_CODE_INTERNAL_ERROR); - } - - /** - * Occurs when the source content cannot be read. - */ - public static function becauseSourceIsUnreadable(SourceExceptionInterface $e): self - { - return new self($e->getMessage(), self::ERROR_CODE_INTERNAL_ERROR, $e); + return new self($message, $source, 0, $e); } } diff --git a/libs/parser/src/Exception/InternalSemanticException.php b/libs/parser/src/Exception/InternalSemanticException.php index 42d6264..7ec8d42 100644 --- a/libs/parser/src/Exception/InternalSemanticException.php +++ b/libs/parser/src/Exception/InternalSemanticException.php @@ -4,18 +4,27 @@ namespace TypeLang\Parser\Exception; +use Phplrt\Contracts\Source\ReadableInterface; + final class InternalSemanticException extends SemanticException { /** - * Occurs when an unexpected sub-node is encountered while building a - * square bracket type and signals a bug in the parser itself. + * Occurs when the grammar builds a sub-node the reducer knows nothing of. * * @param int<0, max> $offset */ - public static function becauseSubNodeIsUnexpected(string $type, int $offset = 0): self - { - $message = \sprintf('Internal error, unexpected square bracket sub-node %s', $type); - - return new self($offset, $message); + public static function becauseSubNodeIsUnexpected( + string $type, + ReadableInterface $source, + int $offset = 0, + ): self { + return new self( + self::describe( + \sprintf('Internal error, unexpected square bracket sub-node %s', $type), + $source, + ), + $source, + self::createToken($source, $offset), + ); } } diff --git a/libs/parser/src/Exception/InvalidConditionalOperatorException.php b/libs/parser/src/Exception/InvalidConditionalOperatorException.php index dcc549f..8bb1f77 100644 --- a/libs/parser/src/Exception/InvalidConditionalOperatorException.php +++ b/libs/parser/src/Exception/InvalidConditionalOperatorException.php @@ -4,18 +4,28 @@ namespace TypeLang\Parser\Exception; +use Phplrt\Contracts\Source\ReadableInterface; + final class InvalidConditionalOperatorException extends SemanticException { /** - * Occurs when a conditional expression uses an unsupported operator. + * Occurs when a condition is written with an operator the grammar knows + * nothing of. * - * @param non-empty-string $operator * @param int<0, max> $offset */ - public static function becauseConditionalOperatorIsInvalid(string $operator, int $offset = 0): self - { - $message = \sprintf('Invalid conditional operator "%s"', $operator); - - return new self($offset, $message, self::ERROR_CODE_INVALID_OPERATOR); + public static function becauseConditionalOperatorIsInvalid( + string $operator, + ReadableInterface $source, + int $offset = 0, + ): self { + return new self( + self::describe( + \sprintf('Invalid conditional operator "%s"', $operator), + $source, + ), + $source, + self::createToken($source, $offset), + ); } } diff --git a/libs/parser/src/Exception/ParseException.php b/libs/parser/src/Exception/ParseException.php deleted file mode 100644 index 82f8b03..0000000 --- a/libs/parser/src/Exception/ParseException.php +++ /dev/null @@ -1,29 +0,0 @@ -getMessage(); + * // => Unexpected token end of input, a shape must be closed + * // with a brace "}" in "array{" + * + * (string) $e; + * // => Unexpected token end of input, a shape must be closed + * // with a brace "}" in "array{" on line 1 at column 7 + * ``` + */ +abstract class ParserException extends \LogicException implements + ParserExceptionInterface +{ + private static ?PrettyTokenPrinter $printer = null; + + public function __construct( + string $message, + public readonly ReadableInterface $source, + int $code = 0, + ?\Throwable $previous = null, + ) { + parent::__construct($message, $code, $previous); + } + + /** + * ``` + * "int" (T_NAME) + * end of input + * "%" (unknown token) + * ``` + */ + protected static function printToken(TokenInterface $token): string + { + return (self::$printer ??= new PrettyTokenPrinter()) + ->print($token); + } + + /** + * ``` + * "array{a: int}" + * "array{a: int, b: str…" (12+) + * + * ``` + */ + protected static function printSource(ReadableInterface $source): string + { + $content = \trim($source->content); + + if ($content === '') { + return ''; + } + + return self::printToken(new Token( + id: 0, + name: null, + channel: Channel::Default, + value: $content, + )); + } +} diff --git a/libs/parser/src/Exception/ParserExceptionInterface.php b/libs/parser/src/Exception/ParserExceptionInterface.php index 658d9f7..537ef35 100644 --- a/libs/parser/src/Exception/ParserExceptionInterface.php +++ b/libs/parser/src/Exception/ParserExceptionInterface.php @@ -4,4 +4,9 @@ namespace TypeLang\Parser\Exception; +use Phplrt\Contracts\Source\ReadableInterface; + +/** + * @property-read ReadableInterface $source + */ interface ParserExceptionInterface extends \Throwable {} diff --git a/libs/parser/src/Exception/ParserRuntimeExceptionInterface.php b/libs/parser/src/Exception/ParserRuntimeExceptionInterface.php new file mode 100644 index 0000000..d3f4706 --- /dev/null +++ b/libs/parser/src/Exception/ParserRuntimeExceptionInterface.php @@ -0,0 +1,15 @@ +getMessage(); + * // => Unexpected token end of input, a shape must be closed + * // with a brace "}" in "array{" + * + * (string) $e; + * // => Unexpected token end of input, a shape must be closed + * // with a brace "}" in "array{" on line 1 at column 7 + * ``` + */ +abstract class ParsingException extends ParserException implements ParserRuntimeExceptionInterface +{ + public PositionInterface $position; + + public function __construct( + string $message, + ReadableInterface $source, + public readonly TokenInterface $token, + int $code = 0, + ?\Throwable $previous = null, + ) { + $this->position = $this->createPosition($source, $token); + + if ($source instanceof FileInterface) { + $pathname = \realpath($source->pathname); + + if ($pathname === false) { + $pathname = \str_replace('\\', '/', $source->pathname); + } + + $this->file = $pathname; + $this->line = $this->position->line; + } + + parent::__construct($message, $source, $code, $previous); + } + + private function createPosition(ReadableInterface $source, TokenInterface $token): PositionInterface + { + try { + return (new PositionFactory()) + ->createFromOffset($source, $token->offset); + } catch (\Throwable) { + return new Position(); + } + } + + /** + * @param int<0, max> $offset + */ + protected static function createToken(ReadableInterface $source, int $offset, int $length = 0): TokenInterface + { + return new Token( + id: 0, + name: null, + channel: Channel::Default, + value: \substr($source->content, $offset, $length), + offset: $offset, + ); + } + + private function printPosition(): string + { + return \sprintf('on line %d at column %d', $this->position->line, $this->position->column); + } + + public function __toString(): string + { + $message = $this->message; + $this->message = \sprintf('%s %s', $message, $this->printPosition()); + + try { + return parent::__toString(); + } finally { + $this->message = $message; + } + } +} diff --git a/libs/parser/src/Exception/SemanticException.php b/libs/parser/src/Exception/SemanticException.php index 575241f..1ebf95d 100644 --- a/libs/parser/src/Exception/SemanticException.php +++ b/libs/parser/src/Exception/SemanticException.php @@ -4,34 +4,17 @@ namespace TypeLang\Parser\Exception; +use Phplrt\Contracts\Source\ReadableInterface; + /** * Error occurring while validating the semantics of a syntactically correct * type statement. */ -abstract class SemanticException extends \LogicException implements ParserExceptionInterface +abstract class SemanticException extends ParsingException { - final public const int ERROR_CODE_SHAPE_KEY_DUPLICATION = 0x01; - - final public const int ERROR_CODE_SHAPE_KEY_MIX = 0x02; - - final public const int ERROR_CODE_VARIADIC_WITH_DEFAULT = 0x03; - - final public const int ERROR_CODE_VARIADIC_ALREADY_VARIADIC = 0x04; - - final public const int ERROR_CODE_INVALID_OPERATOR = 0x05; - - protected const int CODE_LAST = self::ERROR_CODE_INVALID_OPERATOR; - - /** - * @param int<0, max> $offset - */ - final public function __construct( - public readonly int $offset, - string $message, - int $code = 0, - ?\Throwable $previous = null, - ) { - parent::__construct($message, $code, $previous); + protected static function describe(string $message, ReadableInterface $source): string + { + return \sprintf('%s in %s', $message, self::printSource($source)); } /** @@ -39,6 +22,6 @@ final public function __construct( */ public function getOffset(): int { - return $this->offset; + return $this->token->offset; } } diff --git a/libs/parser/src/Exception/SemanticParseException.php b/libs/parser/src/Exception/SemanticParseException.php deleted file mode 100644 index a1b86dd..0000000 --- a/libs/parser/src/Exception/SemanticParseException.php +++ /dev/null @@ -1,28 +0,0 @@ -getMessage()), - Formatter::source($source->getContents()), - Formatter::suffix($source->getContents(), $e->getOffset()), - ]); - - return new self($message, self::ERROR_CODE_SEMANTIC_ERROR_BASE + $e->getCode()); - } -} diff --git a/libs/parser/src/Exception/ShapeFieldDuplicationException.php b/libs/parser/src/Exception/ShapeFieldDuplicationException.php index 0381f6f..50fb0c1 100644 --- a/libs/parser/src/Exception/ShapeFieldDuplicationException.php +++ b/libs/parser/src/Exception/ShapeFieldDuplicationException.php @@ -4,6 +4,8 @@ namespace TypeLang\Parser\Exception; +use Phplrt\Contracts\Source\ReadableInterface; + final class ShapeFieldDuplicationException extends SemanticException { /** @@ -12,10 +14,18 @@ final class ShapeFieldDuplicationException extends SemanticException * @param non-empty-string $key * @param int<0, max> $offset */ - public static function becauseShapeFieldIsDuplicated(string $key, int $offset = 0): self - { - $message = \sprintf('Duplicate key "%s"', $key); - - return new self($offset, $message, self::ERROR_CODE_SHAPE_KEY_DUPLICATION); + public static function becauseShapeFieldIsDuplicated( + string $key, + ReadableInterface $source, + int $offset = 0, + ): self { + return new self( + self::describe( + \sprintf('Duplicate key "%s"', $key), + $source, + ), + $source, + self::createToken($source, $offset), + ); } } diff --git a/libs/parser/src/Exception/ShapeKeyException.php b/libs/parser/src/Exception/ShapeKeyException.php new file mode 100644 index 0000000..194d5bd --- /dev/null +++ b/libs/parser/src/Exception/ShapeKeyException.php @@ -0,0 +1,29 @@ + $offset + */ + public static function becauseKeyIsNotAName( + ReadableInterface $source, + int $offset = 0, + ): self { + return new self( + self::describe( + 'Shape key must be a name, a number, a string or a reference to a constant', + $source, + ), + $source, + self::createToken($source, $offset), + ); + } +} diff --git a/libs/parser/src/Exception/ShapeKeysMixingException.php b/libs/parser/src/Exception/ShapeKeysMixingException.php index c416432..fd82349 100644 --- a/libs/parser/src/Exception/ShapeKeysMixingException.php +++ b/libs/parser/src/Exception/ShapeKeysMixingException.php @@ -4,17 +4,26 @@ namespace TypeLang\Parser\Exception; +use Phplrt\Contracts\Source\ReadableInterface; + final class ShapeKeysMixingException extends SemanticException { /** - * Occurs when a shape mixes explicit and implicit keys. + * Occurs when a shape declares both explicit and implicit keys. * * @param int<0, max> $offset */ - public static function becauseShapeKeysAreMixed(int $offset = 0): self - { - $message = 'Cannot mix explicit and implicit shape keys'; - - return new self($offset, $message, self::ERROR_CODE_SHAPE_KEY_MIX); + public static function becauseShapeKeysAreMixed( + ReadableInterface $source, + int $offset = 0, + ): self { + return new self( + self::describe( + 'Cannot mix explicit and implicit shape keys', + $source, + ), + $source, + self::createToken($source, $offset), + ); } } diff --git a/libs/parser/src/Exception/TemplateBoundException.php b/libs/parser/src/Exception/TemplateBoundException.php new file mode 100644 index 0000000..5620eb2 --- /dev/null +++ b/libs/parser/src/Exception/TemplateBoundException.php @@ -0,0 +1,74 @@ + $offset + */ + public static function becauseOperatorIsUnknown( + string $operator, + ReadableInterface $source, + int $offset = 0, + ): self { + return new self( + self::describe( + \sprintf( + 'Template parameter cannot be bounded with "%s", expected one of "of", "as" or "super"', + $operator, + ), + $source, + ), + $source, + self::createToken($source, $offset), + ); + } + + /** + * Occurs when a template parameter carries the same limit twice. + * + * @param int<0, max> $offset + */ + public static function becauseBoundIsDuplicated( + string $kind, + ReadableInterface $source, + int $offset = 0, + ): self { + return new self( + self::describe( + \sprintf('Template parameter cannot have more than one %s', $kind), + $source, + ), + $source, + self::createToken($source, $offset), + ); + } + + /** + * Occurs when a template parameter carries a bound behind its default. + * + * @param int<0, max> $offset + */ + public static function becauseDefaultIsNotWrittenLast( + ReadableInterface $source, + int $offset = 0, + ): self { + return new self( + self::describe( + 'Template parameter default must be written last, since a bound ' + . 'behind it reads as a bound of the default itself', + $source, + ), + $source, + self::createToken($source, $offset), + ); + } +} diff --git a/libs/parser/src/Exception/UnexpectedTokenException.php b/libs/parser/src/Exception/UnexpectedTokenException.php index bc1f932..ae0e2a4 100644 --- a/libs/parser/src/Exception/UnexpectedTokenException.php +++ b/libs/parser/src/Exception/UnexpectedTokenException.php @@ -4,24 +4,27 @@ namespace TypeLang\Parser\Exception; -use Phplrt\Contracts\Source\SourceExceptionInterface; +use Phplrt\Contracts\Lexer\TokenInterface; +use Phplrt\Contracts\Source\ReadableInterface; -final class UnexpectedTokenException extends ParseException +final class UnexpectedTokenException extends ParsingException { /** * Occurs when a known token is found in an unexpected source location. * - * @param int<0, max> $offset - * @throws SourceExceptionInterface + * The message is the one the grammar carries, whether it is written in + * an "@error" directive or worded by the parser itself. */ - public static function becauseTokenIsUnexpected(string $token, string $statement, int $offset): self - { - $message = \vsprintf('Syntax error, unexpected %s%s %s', [ - Formatter::token($token), - $token === $statement ? '' : ' in ' . Formatter::source($statement), - Formatter::suffix($statement, $offset), + public static function becauseTokenIsUnexpected( + string $message, + ReadableInterface $source, + TokenInterface $token, + ): self { + $message = \vsprintf('%s in %s', [ + $message, + self::printSource($source), ]); - return new self($message, self::ERROR_CODE_UNEXPECTED_TOKEN); + return new self($message, $source, $token); } } diff --git a/libs/parser/src/Exception/UnreadableSourceException.php b/libs/parser/src/Exception/UnreadableSourceException.php new file mode 100644 index 0000000..b646707 --- /dev/null +++ b/libs/parser/src/Exception/UnreadableSourceException.php @@ -0,0 +1,21 @@ +getMessage(), $source, 0, $e); + } +} diff --git a/libs/parser/src/Exception/UnrecognizedSyntaxException.php b/libs/parser/src/Exception/UnrecognizedSyntaxException.php index a6b6e83..3ea50e8 100644 --- a/libs/parser/src/Exception/UnrecognizedSyntaxException.php +++ b/libs/parser/src/Exception/UnrecognizedSyntaxException.php @@ -4,24 +4,19 @@ namespace TypeLang\Parser\Exception; -use Phplrt\Contracts\Source\SourceExceptionInterface; +use Phplrt\Contracts\Lexer\TokenInterface; +use Phplrt\Contracts\Source\ReadableInterface; -final class UnrecognizedSyntaxException extends ParseException +final class UnrecognizedSyntaxException extends ParsingException { /** * Occurs when the parser reaches a state that does not match any known * grammar rule. - * - * @param int<0, max> $offset - * @throws SourceExceptionInterface */ - public static function becauseSyntaxIsUnrecognized(string $statement, int $offset): self + public static function becauseSyntaxIsUnrecognized(ReadableInterface $source, TokenInterface $token): self { - $message = \vsprintf('Internal syntax error, in %s %s', [ - Formatter::source($statement), - Formatter::suffix($statement, $offset), - ]); + $message = \sprintf('Internal syntax error in %s', self::printSource($source)); - return new self($message, self::ERROR_CODE_UNEXPECTED_SYNTAX_ERROR); + return new self($message, $source, $token); } } diff --git a/libs/parser/src/Exception/UnrecognizedTokenException.php b/libs/parser/src/Exception/UnrecognizedTokenException.php index 5973c13..ea9e04a 100644 --- a/libs/parser/src/Exception/UnrecognizedTokenException.php +++ b/libs/parser/src/Exception/UnrecognizedTokenException.php @@ -4,24 +4,21 @@ namespace TypeLang\Parser\Exception; -use Phplrt\Contracts\Source\SourceExceptionInterface; +use Phplrt\Contracts\Lexer\TokenInterface; +use Phplrt\Contracts\Source\ReadableInterface; -final class UnrecognizedTokenException extends ParseException +final class UnrecognizedTokenException extends ParsingException { /** * Occurs when unable to recognize tokens in source code. - * - * @param int<0, max> $offset - * @throws SourceExceptionInterface */ - public static function becauseTokenIsUnrecognized(string $token, string $statement, int $offset): self + public static function becauseTokenIsUnrecognized(ReadableInterface $source, TokenInterface $token): self { - $message = \vsprintf('Syntax error, unrecognized %s%s %s', [ - Formatter::token($token), - $token === $statement ? '' : ' in ' . Formatter::source($statement), - Formatter::suffix($statement, $offset), + $message = \vsprintf('Syntax error, unexpected %s in %s', [ + self::printToken($token), + self::printSource($source), ]); - return new self($message, self::ERROR_CODE_UNRECOGNIZED_TOKEN); + return new self($message, $source, $token); } } diff --git a/libs/parser/src/Exception/VariadicRedefinitionException.php b/libs/parser/src/Exception/VariadicRedefinitionException.php deleted file mode 100644 index 9f88f5e..0000000 --- a/libs/parser/src/Exception/VariadicRedefinitionException.php +++ /dev/null @@ -1,20 +0,0 @@ - $offset - */ - public static function becauseVariadicIsRedefined(int $offset = 0): self - { - $message = 'Either prefix or postfix variadic syntax should be used, but not both'; - - return new self($offset, $message, self::ERROR_CODE_VARIADIC_ALREADY_VARIADIC); - } -} diff --git a/libs/parser/src/Exception/VariadicWithDefaultException.php b/libs/parser/src/Exception/VariadicWithDefaultException.php index 9f85b44..11b9a99 100644 --- a/libs/parser/src/Exception/VariadicWithDefaultException.php +++ b/libs/parser/src/Exception/VariadicWithDefaultException.php @@ -4,17 +4,26 @@ namespace TypeLang\Parser\Exception; +use Phplrt\Contracts\Source\ReadableInterface; + final class VariadicWithDefaultException extends SemanticException { /** - * Occurs when a variadic parameter also declares a default value. + * Occurs when a variadic parameter is written with a default. * * @param int<0, max> $offset */ - public static function becauseVariadicHasDefault(int $offset = 0): self - { - $message = 'Cannot have variadic param with a default'; - - return new self($offset, $message, self::ERROR_CODE_VARIADIC_WITH_DEFAULT); + public static function becauseVariadicHasDefault( + ReadableInterface $source, + int $offset = 0, + ): self { + return new self( + self::describe( + 'Cannot have variadic param with a default', + $source, + ), + $source, + self::createToken($source, $offset), + ); } } diff --git a/libs/parser/src/InMemoryTypeParser.php b/libs/parser/src/InMemoryTypeParser.php index 05b10f3..5f87b1f 100644 --- a/libs/parser/src/InMemoryTypeParser.php +++ b/libs/parser/src/InMemoryTypeParser.php @@ -5,14 +5,22 @@ namespace TypeLang\Parser; use JetBrains\PhpStorm\Language; -use Phplrt\Contracts\Source\SourceExceptionInterface; +use Phplrt\Contracts\Source\Exception\SourceExceptionInterface; +use Phplrt\Contracts\Source\ReadableInterface; use Phplrt\Contracts\Source\SourceFactoryInterface; use Phplrt\Source\SourceFactory; use TypeLang\Parser\Exception\ParserExceptionInterface; +use TypeLang\Parser\Partial\ParsedResult; +use TypeLang\Parser\Validation\CheckResult; use TypeLang\Type\TypeNode; final class InMemoryTypeParser implements TypeParserInterface { + /** + * @var non-empty-string + */ + private const HASH_ALGORITHM = 'xxh128'; + /** * @var array */ @@ -23,10 +31,26 @@ final class InMemoryTypeParser implements TypeParserInterface */ private array $sequences = []; + /** + * @var array + */ + private array $checks = []; + + private readonly SourceFactoryInterface $sources; + public function __construct( private readonly TypeParserInterface $parser = new TypeParser(), - private readonly SourceFactoryInterface $sources = new SourceFactory(), - ) {} + ?SourceFactoryInterface $sources = null, + ) { + $this->sources = $sources ?? SourceFactory::createDefault(); + } + + public function reset(): void + { + $this->types = []; + $this->sequences = []; + $this->checks = []; + } /** * @throws ParserExceptionInterface @@ -37,7 +61,19 @@ public function parse(#[Language('PHP')] mixed $source): TypeNode { $instance = $this->sources->create($source); - return $this->types[$instance->getHash()] ??= $this->parser->parse($source); + return $this->types[$this->hash($instance)] ??= $this->parser->parse($source); + } + + /** + * @throws ParserExceptionInterface + * @throws SourceExceptionInterface + * @throws \Throwable + */ + public function partial(#[Language('PHP')] mixed $source): ParsedResult + { + $instance = $this->sources->create($source); + + return $this->sequences[$this->hash($instance)] ??= $this->parser->partial($source); } /** @@ -45,10 +81,18 @@ public function parse(#[Language('PHP')] mixed $source): TypeNode * @throws SourceExceptionInterface * @throws \Throwable */ - public function parseTolerant(#[Language('PHP')] mixed $source): ParsedResult + public function validate(#[Language('PHP')] mixed $source): CheckResult { $instance = $this->sources->create($source); - return $this->sequences[$instance->getHash()] ??= $this->parser->parseTolerant($source); + return $this->checks[$this->hash($instance)] ??= $this->parser->validate($source); + } + + /** + * @return non-empty-string + */ + private function hash(ReadableInterface $source): string + { + return \hash(self::HASH_ALGORITHM, $source->content); } } diff --git a/libs/parser/src/Internal/CompiledExecutor.php b/libs/parser/src/Internal/CompiledExecutor.php new file mode 100644 index 0000000..9782900 --- /dev/null +++ b/libs/parser/src/Internal/CompiledExecutor.php @@ -0,0 +1,2880 @@ + + */ +abstract class CompiledExecutor implements \Phplrt\Contracts\Parser\ParserInterface +{ + /** @var int */ + public const T_WHITESPACE = 0; + /** @var int */ + public const T_EQ = 1; + /** @var int */ + public const T_NOT = 2; + /** @var int */ + public const T_TRUE_LITERAL = 3; + /** @var int */ + public const T_FALSE_LITERAL = 4; + /** @var int */ + public const T_NULL_LITERAL = 5; + /** @var int */ + public const T_NAME_WITH_SPACE = 6; + /** @var int */ + public const T_NAME = 7; + /** @var int */ + public const T_NS_DELIMITER = 8; + /** @var int */ + public const T_OR = 9; + /** @var int */ + public const T_COMMA = 10; + /** @var int */ + public const T_DOUBLE_COLON = 11; + /** @var int */ + public const T_COLON = 12; + /** @var int */ + public const T_ANGLE_BRACKET_OPEN = 13; + /** @var int */ + public const T_ANGLE_BRACKET_CLOSE = 14; + /** @var int */ + public const T_QMARK = 15; + /** @var int */ + public const T_BRACE_OPEN = 16; + /** @var int */ + public const T_BRACE_CLOSE = 17; + /** @var int */ + public const T_PARENTHESIS_OPEN = 18; + /** @var int */ + public const T_PARENTHESIS_CLOSE = 19; + /** @var int */ + public const T_SQUARE_BRACKET_OPEN = 20; + /** @var int */ + public const T_SQUARE_BRACKET_CLOSE = 21; + /** @var int */ + public const T_AMP = 22; + /** @var int */ + public const T_ELLIPSIS = 23; + /** @var int */ + public const T_ASSIGN = 24; + /** @var int */ + public const T_ASTERISK = 25; + /** @var int */ + public const T_THIS = 26; + /** @var int */ + public const T_VARIABLE = 27; + /** @var int */ + public const T_DQ_STRING_LITERAL = 28; + /** @var int */ + public const T_SQ_STRING_LITERAL = 29; + /** @var int */ + public const T_FLOAT_LITERAL = 30; + /** @var int */ + public const T_BIN_INT_LITERAL = 31; + /** @var int */ + public const T_OCT_INT_LITERAL = 32; + /** @var int */ + public const T_HEX_INT_LITERAL = 33; + /** @var int */ + public const T_DEC_INT_LITERAL = 34; + /** @var int */ + public const T_COMMENT = 35; + /** @var int */ + public const T_DOC_COMMENT = 36; + + /** @var array */ + private const PARSER_EXPECTATIONS = [ + 'T_WHITESPACE', + 'T_EQ', + 'T_NOT', + 'T_TRUE_LITERAL', + 'T_FALSE_LITERAL', + 'T_NULL_LITERAL', + 'T_NAME_WITH_SPACE', + 'T_NAME', + 'T_NS_DELIMITER', + 'T_OR', + 'T_COMMA', + 'T_DOUBLE_COLON', + 'T_COLON', + 'T_ANGLE_BRACKET_OPEN', + 'T_ANGLE_BRACKET_CLOSE', + 'T_QMARK', + 'T_BRACE_OPEN', + 'T_BRACE_CLOSE', + 'T_PARENTHESIS_OPEN', + 'T_PARENTHESIS_CLOSE', + 'T_SQUARE_BRACKET_OPEN', + 'T_SQUARE_BRACKET_CLOSE', + 'T_AMP', + 'T_ELLIPSIS', + 'T_ASSIGN', + 'T_ASTERISK', + 'T_THIS', + 'T_VARIABLE', + 'T_DQ_STRING_LITERAL', + 'T_SQ_STRING_LITERAL', + 'T_FLOAT_LITERAL', + 'T_BIN_INT_LITERAL', + 'T_OCT_INT_LITERAL', + 'T_HEX_INT_LITERAL', + 'T_DEC_INT_LITERAL', + 'T_COMMENT', + 'T_DOC_COMMENT', + '/[^\\s]++/', + ]; + + /** @var array */ + private const PARSER_MESSAGES = [ + 5 => 'Syntax error, unexpected {token}, a nullable type must carry the type it makes nullable, for example "?T"', + 10 => 'Syntax error, unexpected {token}, a group must be closed with a bracket ")"', + 41 => 'Syntax error, unexpected {token}, a name must carry a segment after the separator, for example "Some\\Any"', + 46 => 'Syntax error, unexpected {token}, a class constant must carry a name after the double colon, for example "Some::CONST"', + 72 => 'Syntax error, unexpected {token}, a parameter list must be closed with a bracket ")"', + 76 => 'Syntax error, unexpected {token}, a callable must carry a type after the colon ":"', + 80 => 'Syntax error, unexpected {token}, an argument list must carry at least one argument', + 87 => 'Syntax error, unexpected {token}, an argument list must be closed with a bracket ">"', + 91 => 'Syntax error, unexpected {token}, a parameter list must carry at least one parameter', + 100 => 'Syntax error, unexpected {token}, a parameter list must be closed with a bracket ">"', + 115 => 'Syntax error, unexpected {token}, a shape field must carry a type after the colon ":"', + 124 => 'Syntax error, unexpected {token}, a shape must be closed with a brace "}"', + 131 => 'Syntax error, unexpected {token}, an offset must be closed with a bracket "]"', + 136 => 'Syntax error, unexpected {token}, a union type must carry a type after the vertical bar "|", for example "T|U"', + 138 => 'Syntax error, unexpected {token}, an intersection type must carry a type after the ampersand "&", for example "T&U"', + 144 => 'Syntax error, unexpected {token}, a condition must carry the type it is true of', + 'Syntax error, unexpected {token}, a condition must be parted with a colon ":"', + 'Syntax error, unexpected {token}, a condition must carry the type it is false of', + ]; + + /** @var array */ + private const PARSER_KEPT_TABLE = [ + true, + true, + false, + true, + false, + false, + true, + false, + false, + false, + false, + true, + true, + false, + false, + true, + true, + true, + true, + false, + false, + false, + false, + true, + true, + true, + true, + true, + false, + false, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + true, + true, + true, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + true, + true, + true, + false, + false, + false, + false, + false, + false, + true, + false, + true, + false, + false, + true, + false, + false, + false, + false, + false, + true, + false, + false, + false, + true, + false, + true, + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false, + true, + ]; + + /** @var array|null> */ + private const PARSER_START_PREDICTION_TABLE = [ + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 15 => true, + ], + [ + 15 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 18 => true, + ], + [ + 18 => true, + ], + [ + 19 => true, + ], + [ + 26 => true, + ], + [ + 3 => true, + true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 3 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 28 => true, + true, + ], + [ + 28 => true, + ], + [ + 29 => true, + ], + [ + 30 => true, + ], + [ + 31 => true, + true, + true, + true, + ], + [ + 31 => true, + ], + [ + 32 => true, + ], + [ + 33 => true, + ], + [ + 34 => true, + ], + [ + 3 => true, + ], + [ + 4 => true, + ], + [ + 5 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + ], + null, + [ + 8 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 7 => true, + ], + [ + 6 => true, + ], + [ + 1 => true, + ], + [ + 2 => true, + ], + [ + 3 => true, + ], + [ + 4 => true, + ], + [ + 5 => true, + ], + null, + [ + 8 => true, + ], + [ + 8 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + ], + null, + [ + 8 => true, + 11 => true, + 13 => true, + 16 => true, + 18 => true, + 25 => true, + ], + [ + 11 => true, + ], + [ + 11 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + 25 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + ], + null, + [ + 25 => true, + ], + [ + 25 => true, + ], + null, + [ + 1 => true, + true, + true, + true, + true, + true, + true, + ], + null, + [ + 18 => true, + ], + null, + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + null, + [ + 22 => true, + ], + null, + [ + 23 => true, + ], + null, + [ + 26 => true, + true, + ], + [ + 27 => true, + ], + [ + 26 => true, + ], + null, + [ + 24 => true, + ], + null, + [ + 10 => true, + ], + [ + 10 => true, + ], + null, + [ + 19 => true, + ], + null, + [ + 12 => true, + ], + [ + 12 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 13 => true, + ], + [ + 13 => true, + ], + [ + 13 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 6 => true, + ], + [ + 6 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + null, + [ + 10 => true, + ], + [ + 14 => true, + ], + null, + [ + 13 => true, + ], + [ + 13 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + ], + null, + [ + 1 => true, + true, + true, + true, + true, + true, + true, + 24 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 24 => true, + ], + [ + 24 => true, + ], + null, + [ + 10 => true, + ], + [ + 14 => true, + ], + [ + 16 => true, + ], + [ + 16 => true, + ], + null, + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + ], + null, + [ + 12 => true, + 15 => true, + ], + null, + null, + [ + 15 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + null, + [ + 10 => true, + ], + null, + [ + 10 => true, + ], + [ + 23 => true, + ], + null, + null, + [ + 17 => true, + ], + [ + 8 => true, + ], + [ + 25 => true, + ], + null, + [ + 20 => true, + ], + [ + 20 => true, + ], + null, + [ + 21 => true, + ], + null, + [ + 9 => true, + 22 => true, + ], + [ + 9 => true, + ], + [ + 9 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + [ + 22 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + 28 => true, + true, + true, + true, + true, + true, + true, + ], + null, + [ + 1 => true, + ], + [ + 1 => true, + ], + null, + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 12 => true, + ], + [ + 1 => true, + true, + true, + true, + true, + true, + true, + true, + 15 => true, + 18 => true, + 25 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 26 => true, + true, + ], + ]; + + /** @var array>> */ + private const PARSER_CHOICE_PREDICTION_TABLE = [ + [ + 1 => [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + 15 => [ + 1, + ], + 18 => [ + 1, + ], + 25 => [ + 1, + ], + 28 => [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + [ + 1, + ], + 27 => [ + 147, + ], + ], + 2 => [ + 15 => [ + 3, + ], + 1 => [ + 6, + ], + 2 => [ + 6, + ], + 3 => [ + 6, + ], + 4 => [ + 6, + ], + 5 => [ + 6, + ], + 6 => [ + 6, + ], + 7 => [ + 6, + ], + 8 => [ + 6, + ], + 18 => [ + 6, + ], + 25 => [ + 6, + ], + [ + 6, + ], + 28 => [ + 6, + ], + [ + 6, + ], + [ + 6, + ], + [ + 6, + ], + [ + 6, + ], + [ + 6, + ], + [ + 6, + ], + ], + 7 => [ + 18 => [ + 8, + ], + 26 => [ + -12, + ], + 3 => [ + 12, + 26, + ], + 4 => [ + 12, + 26, + ], + 5 => [ + 12, + 26, + ], + 28 => [ + 12, + ], + [ + 12, + ], + [ + 12, + ], + [ + 12, + ], + [ + 12, + ], + [ + 12, + ], + [ + 12, + ], + 1 => [ + 26, + ], + 2 => [ + 26, + ], + 6 => [ + 26, + ], + 7 => [ + 26, + ], + 8 => [ + 26, + ], + 25 => [ + 126, + ], + ], + 12 => [ + 3 => [ + 13, + ], + [ + 13, + ], + 28 => [ + 13, + ], + [ + 13, + ], + [ + 13, + ], + [ + 13, + ], + [ + 13, + ], + [ + 13, + ], + [ + 13, + ], + 5 => [ + -26, + ], + ], + [ + 28 => [ + 14, + ], + [ + 14, + ], + [ + -18, + ], + [ + 18, + ], + [ + 18, + ], + [ + 18, + ], + [ + 18, + ], + 3 => [ + -24, + ], + 4 => [ + -25, + ], + ], + [ + 28 => [ + -16, + ], + [ + -17, + ], + ], + 18 => [ + 31 => [ + -20, + ], + [ + -21, + ], + [ + -22, + ], + [ + -23, + ], + ], + 30 => [ + 7 => [ + -32, + ], + 6 => [ + -33, + ], + 1 => [ + -34, + ], + 2 => [ + -35, + ], + 3 => [ + -36, + ], + 4 => [ + -37, + ], + 5 => [ + -38, + ], + ], + 43 => [ + 11 => [ + 44, + ], + 18 => [ + 54, + ], + 13 => [ + 77, + 89, + ], + 16 => [ + 101, + ], + 25 => [ + 49, + ], + 8 => [ + 125, + ], + ], + 46 => [ + 1 => [ + 47, + ], + [ + 47, + ], + [ + 47, + ], + [ + 47, + ], + [ + 47, + ], + [ + 47, + ], + [ + 47, + ], + 25 => [ + 49, + ], + ], + 63 => [ + 27 => [ + -65, + ], + 26 => [ + -66, + ], + ], + 81 => [ + 1 => [ + 84, + ], + [ + 84, + ], + [ + 84, + ], + [ + 84, + ], + [ + 84, + ], + 7 => [ + 84, + ], + [ + 84, + ], + 15 => [ + 84, + ], + 18 => [ + 84, + ], + 25 => [ + 84, + ], + [ + 84, + ], + [ + 84, + ], + [ + 84, + ], + [ + 84, + ], + [ + 84, + ], + [ + 84, + ], + [ + 84, + ], + [ + 84, + ], + [ + 84, + ], + ], + 84 => [ + 25 => [ + -51, + 0, + ], + 1 => [ + 0, + ], + 2 => [ + 0, + ], + 3 => [ + 0, + ], + 4 => [ + 0, + ], + 5 => [ + 0, + ], + 6 => [ + 0, + ], + 7 => [ + 0, + ], + 8 => [ + 0, + ], + 15 => [ + 0, + ], + 18 => [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + ], + 94 => [ + 1 => [ + 95, + ], + [ + 95, + ], + [ + 95, + ], + [ + 95, + ], + [ + 95, + ], + [ + 95, + ], + [ + 95, + ], + 24 => [ + 96, + ], + ], + 106 => [ + 15 => [ + 116, + ], + 27 => [ + 116, + ], + ], + 108 => [ + 8 => [ + 7, + ], + 18 => [ + 7, + ], + 25 => [ + 7, + ], + [ + 7, + ], + 28 => [ + 7, + ], + [ + 7, + ], + [ + 7, + ], + [ + 7, + ], + [ + 7, + ], + [ + 7, + ], + [ + 7, + ], + ], + 133 => [ + 9 => [ + 134, + ], + 22 => [ + 137, + ], + ], + 143 => [ + 1 => [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + 15 => [ + 0, + ], + 18 => [ + 0, + ], + 25 => [ + 0, + ], + 28 => [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + [ + 0, + ], + ], + ]; + + /** @var array> */ + private const PARSER_SEQUENCE_PREDICTION_TABLE = [ + 1 => [ + 2, + 132, + -141, + ], + 26 => [ + 27, + -44, + ], + [ + -30, + 30, + 38, + ], + 47 => [ + 30, + -50, + ], + 49 => [ + 50, + 51, + -31, + ], + 54 => [ + 9, + 55, + 72, + -75, + ], + 56 => [ + 57, + 68, + -71, + ], + [ + 0, + -60, + -62, + -64, + -68, + ], + 78 => [ + 79, + 80, + 85, + -71, + 87, + ], + 90 => [ + 79, + 91, + 98, + -71, + 100, + ], + 101 => [ + 102, + 103, + -71, + 124, + ], + 104 => [ + 105, + -121, + ], + 107 => [ + 108, + -115, + 75, + 115, + ], + 111 => [ + -5, + 75, + ], + 121 => [ + 61, + -79, + ], + 126 => [ + 50, + 30, + -50, + ], + 128 => [ + 129, + -1, + 131, + ], + 141 => [ + 33, + -35, + ], + ]; + + /** + * @var \Phplrt\Parser\Parser + */ + protected readonly \Phplrt\Parser\Parser $parser; + + protected readonly \Phplrt\Contracts\Lexer\LexerInterface $lexer; + + public function __construct() + { + $this->lexer = new \Phplrt\Lexer\Lexer( + pattern: '/\\G(?|(?:(?:\\s++)(*MARK:0))|(?:(?:is(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:1))|(?:(?:not(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:2))|(?:(?:(?i:true)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:3))|(?:(?:(?i:false)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:4))|(?:(?:(?i:null)(?:(?!(?:[a-zA-Z0-9\\-_\\x80-\\xff]))))(*MARK:5))|(?:(?:(?:[a-zA-Z_\\x80-\\xff])(?:[a-zA-Z0-9\\-_\\x80-\\xff])*+\\s++)(*MARK:6))|(?:(?:(?:[a-zA-Z_\\x80-\\xff])(?:[a-zA-Z0-9\\-_\\x80-\\xff])*+)(*MARK:7))|(?:(?:\\\\)(*MARK:8))|(?:(?:\\|)(*MARK:9))|(?:(?:,)(*MARK:10))|(?:(?:::)(*MARK:11))|(?:(?::)(*MARK:12))|(?:(?:<)(*MARK:13))|(?:(?:>)(*MARK:14))|(?:(?:\\?)(*MARK:15))|(?:(?:\\{)(*MARK:16))|(?:(?:\\})(*MARK:17))|(?:(?:\\()(*MARK:18))|(?:(?:\\))(*MARK:19))|(?:(?:\\[)(*MARK:20))|(?:(?:\\])(*MARK:21))|(?:(?:&)(*MARK:22))|(?:(?:\\.\\.\\.)(*MARK:23))|(?:(?:=)(*MARK:24))|(?:(?:\\*)(*MARK:25))|(?:(?:\\$this(?!(?:[a-zA-Z0-9_\\x80-\\xff])))(*MARK:26))|(?:(?:\\$(?:[a-zA-Z_\\x80-\\xff])(?:[a-zA-Z0-9_\\x80-\\xff])*+)(*MARK:27))|(?:(?:"[^"\\\\]*+(?:\\\\.[^"\\\\]*+)*+")(*MARK:28))|(?:(?:\'[^\'\\\\]*+(?:\\\\.[^\'\\\\]*+)*+\')(*MARK:29))|(?:(?:(?:[-+])?(?:(?:(?:(?:[0-9])(?:_?(?:[0-9]))*+)\\.(?:(?:(?:[0-9])(?:_?(?:[0-9]))*+))?|\\.(?:(?:[0-9])(?:_?(?:[0-9]))*+))(?:(?:[eE](?:[-+])?(?:(?:[0-9])(?:_?(?:[0-9]))*+)))?|(?:(?:[0-9])(?:_?(?:[0-9]))*+)(?:[eE](?:[-+])?(?:(?:[0-9])(?:_?(?:[0-9]))*+))))(*MARK:30))|(?:(?:(?:[-+])?0[bB](?:(?:[01])(?:_?(?:[01]))*+))(*MARK:31))|(?:(?:(?:[-+])?0(?:[oO](?:(?:[0-7])(?:_?(?:[0-7]))*+)|(?:_?(?:[0-7]))++))(*MARK:32))|(?:(?:(?:[-+])?0[xX](?:(?:[0-9a-fA-F])(?:_?(?:[0-9a-fA-F]))*+))(*MARK:33))|(?:(?:(?:[-+])?(?:(?:(?:[1-9])(?:_?(?:[0-9]))*+)|0))(*MARK:34))|(?:(?:(?:\\/\\/|\\#)[^\\r\\n]*+)(*MARK:35))|(?:(?:\\/\\*.*?\\*\\/)(*MARK:36))|(?:(?:[^\\s]++)(*MARK:37)))/Ssm', + channels: [ + 'Hidden', + 35 => 'Hidden', + 'Hidden', + 'Unknown', + ], + names: [ + 'T_WHITESPACE', + 'T_EQ', + 'T_NOT', + 'T_TRUE_LITERAL', + 'T_FALSE_LITERAL', + 'T_NULL_LITERAL', + 'T_NAME_WITH_SPACE', + 'T_NAME', + 'T_NS_DELIMITER', + 'T_OR', + 'T_COMMA', + 'T_DOUBLE_COLON', + 'T_COLON', + 'T_ANGLE_BRACKET_OPEN', + 'T_ANGLE_BRACKET_CLOSE', + 'T_QMARK', + 'T_BRACE_OPEN', + 'T_BRACE_CLOSE', + 'T_PARENTHESIS_OPEN', + 'T_PARENTHESIS_CLOSE', + 'T_SQUARE_BRACKET_OPEN', + 'T_SQUARE_BRACKET_CLOSE', + 'T_AMP', + 'T_ELLIPSIS', + 'T_ASSIGN', + 'T_ASTERISK', + 'T_THIS', + 'T_VARIABLE', + 'T_DQ_STRING_LITERAL', + 'T_SQ_STRING_LITERAL', + 'T_FLOAT_LITERAL', + 'T_BIN_INT_LITERAL', + 'T_OCT_INT_LITERAL', + 'T_HEX_INT_LITERAL', + 'T_DEC_INT_LITERAL', + 'T_COMMENT', + 'T_DOC_COMMENT', + ], + ); + + $this->parser = new \Phplrt\Parser\Parser( + lexer: $this->lexer, + grammar: [ + new \Phplrt\Parser\Grammar\Alternation([1, 147]), + new \Phplrt\Parser\Grammar\Concatenation([2, 132, 139]), + new \Phplrt\Parser\Grammar\Alternation([3, 6]), + new \Phplrt\Parser\Grammar\Concatenation([4, 5]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_QMARK, false), + new \Phplrt\Parser\Grammar\Concatenation([6]), + new \Phplrt\Parser\Grammar\Concatenation([7, 127]), + new \Phplrt\Parser\Grammar\Alternation([8, 11, 12, 26, 126]), + new \Phplrt\Parser\Grammar\Concatenation([9, 0, 10]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_PARENTHESIS_OPEN, false), + new \Phplrt\Parser\Grammar\Lexeme(self::T_PARENTHESIS_CLOSE, false), + new \Phplrt\Parser\Grammar\Lexeme(self::T_THIS, true), + new \Phplrt\Parser\Grammar\Alternation([13, 25]), + new \Phplrt\Parser\Grammar\Alternation([14, 17, 18, 23, 24]), + new \Phplrt\Parser\Grammar\Alternation([15, 16]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_DQ_STRING_LITERAL, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_SQ_STRING_LITERAL, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_FLOAT_LITERAL, true), + new \Phplrt\Parser\Grammar\Alternation([19, 20, 21, 22]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_BIN_INT_LITERAL, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_OCT_INT_LITERAL, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_HEX_INT_LITERAL, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_DEC_INT_LITERAL, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_TRUE_LITERAL, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_FALSE_LITERAL, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NULL_LITERAL, true), + new \Phplrt\Parser\Grammar\Concatenation([27, 42]), + new \Phplrt\Parser\Grammar\Concatenation([28, 30, 38]), + new \Phplrt\Parser\Grammar\Optional(29), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NS_DELIMITER, true), + new \Phplrt\Parser\Grammar\Alternation([31, 32, 33, 34, 35, 36, 37]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NAME, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NAME_WITH_SPACE, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_EQ, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NOT, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_TRUE_LITERAL, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_FALSE_LITERAL, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NULL_LITERAL, true), + new \Phplrt\Parser\Grammar\Repetition(39, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([40, 41]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NS_DELIMITER, false), + new \Phplrt\Parser\Grammar\Concatenation([30]), + new \Phplrt\Parser\Grammar\Optional(43), + new \Phplrt\Parser\Grammar\Alternation([44, 54, 77, 89, 101, 49, 125]), + new \Phplrt\Parser\Grammar\Concatenation([45, 46]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_DOUBLE_COLON, true), + new \Phplrt\Parser\Grammar\Alternation([47, 49]), + new \Phplrt\Parser\Grammar\Concatenation([30, 48]), + new \Phplrt\Parser\Grammar\Optional(49), + new \Phplrt\Parser\Grammar\Concatenation([50, 51, 53]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ASTERISK, true), + new \Phplrt\Parser\Grammar\Repetition(52, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([30, 50]), + new \Phplrt\Parser\Grammar\Optional(30), + new \Phplrt\Parser\Grammar\Concatenation([9, 55, 72, 73]), + new \Phplrt\Parser\Grammar\Optional(56), + new \Phplrt\Parser\Grammar\Concatenation([57, 68, 71]), + new \Phplrt\Parser\Grammar\Concatenation([0, 58, 60, 62, 66]), + new \Phplrt\Parser\Grammar\Optional(59), + new \Phplrt\Parser\Grammar\Lexeme(self::T_AMP, true), + new \Phplrt\Parser\Grammar\Optional(61), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ELLIPSIS, true), + new \Phplrt\Parser\Grammar\Optional(63), + new \Phplrt\Parser\Grammar\Alternation([64, 65]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_VARIABLE, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_THIS, true), + new \Phplrt\Parser\Grammar\Optional(67), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ASSIGN, true), + new \Phplrt\Parser\Grammar\Repetition(69, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([70, 57]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_COMMA, false), + new \Phplrt\Parser\Grammar\Optional(70), + new \Phplrt\Parser\Grammar\Lexeme(self::T_PARENTHESIS_CLOSE, false), + new \Phplrt\Parser\Grammar\Optional(74), + new \Phplrt\Parser\Grammar\Concatenation([75, 76]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_COLON, false), + new \Phplrt\Parser\Grammar\Concatenation([0]), + new \Phplrt\Parser\Grammar\Concatenation([78, 88]), + new \Phplrt\Parser\Grammar\Concatenation([79, 80, 85, 71, 87]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_OPEN, false), + new \Phplrt\Parser\Grammar\Concatenation([81]), + new \Phplrt\Parser\Grammar\Alternation([82, 84]), + new \Phplrt\Parser\Grammar\Concatenation([83, 84]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NAME_WITH_SPACE, true), + new \Phplrt\Parser\Grammar\Alternation([50, 0]), + new \Phplrt\Parser\Grammar\Repetition(86, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([70, 81]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_CLOSE, false), + new \Phplrt\Parser\Grammar\Predicate(9, false), + new \Phplrt\Parser\Grammar\Concatenation([90, 54]), + new \Phplrt\Parser\Grammar\Concatenation([79, 91, 98, 71, 100]), + new \Phplrt\Parser\Grammar\Concatenation([92]), + new \Phplrt\Parser\Grammar\Concatenation([30, 93]), + new \Phplrt\Parser\Grammar\Repetition(94, 0, \INF), + new \Phplrt\Parser\Grammar\Alternation([95, 96]), + new \Phplrt\Parser\Grammar\Concatenation([30, 0]), + new \Phplrt\Parser\Grammar\Concatenation([97, 0]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ASSIGN, false), + new \Phplrt\Parser\Grammar\Repetition(99, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([70, 92]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ANGLE_BRACKET_CLOSE, false), + new \Phplrt\Parser\Grammar\Concatenation([102, 103, 71, 124]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_BRACE_OPEN, false), + new \Phplrt\Parser\Grammar\Alternation([104, 123]), + new \Phplrt\Parser\Grammar\Concatenation([105, 119]), + new \Phplrt\Parser\Grammar\Concatenation([106, 117]), + new \Phplrt\Parser\Grammar\Alternation([107, 116]), + new \Phplrt\Parser\Grammar\Concatenation([108, 113, 75, 115]), + new \Phplrt\Parser\Grammar\Alternation([109, 7]), + new \Phplrt\Parser\Grammar\Concatenation([30, 110]), + new \Phplrt\Parser\Grammar\Predicate(111, true), + new \Phplrt\Parser\Grammar\Concatenation([112, 75]), + new \Phplrt\Parser\Grammar\Optional(4), + new \Phplrt\Parser\Grammar\Optional(114), + new \Phplrt\Parser\Grammar\Lexeme(self::T_QMARK, true), + new \Phplrt\Parser\Grammar\Concatenation([0]), + new \Phplrt\Parser\Grammar\Concatenation([0]), + new \Phplrt\Parser\Grammar\Repetition(118, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([70, 106]), + new \Phplrt\Parser\Grammar\Optional(120), + new \Phplrt\Parser\Grammar\Concatenation([70, 121]), + new \Phplrt\Parser\Grammar\Concatenation([61, 122]), + new \Phplrt\Parser\Grammar\Optional(78), + new \Phplrt\Parser\Grammar\Optional(121), + new \Phplrt\Parser\Grammar\Lexeme(self::T_BRACE_CLOSE, false), + new \Phplrt\Parser\Grammar\Concatenation([40, 49]), + new \Phplrt\Parser\Grammar\Concatenation([50, 30, 48]), + new \Phplrt\Parser\Grammar\Repetition(128, 0, \INF), + new \Phplrt\Parser\Grammar\Concatenation([129, 130, 131]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_SQUARE_BRACKET_OPEN, false), + new \Phplrt\Parser\Grammar\Optional(0), + new \Phplrt\Parser\Grammar\Lexeme(self::T_SQUARE_BRACKET_CLOSE, false), + new \Phplrt\Parser\Grammar\Repetition(133, 0, \INF), + new \Phplrt\Parser\Grammar\Alternation([134, 137]), + new \Phplrt\Parser\Grammar\Concatenation([135, 136]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_OR, true), + new \Phplrt\Parser\Grammar\Concatenation([2]), + new \Phplrt\Parser\Grammar\Concatenation([59, 138]), + new \Phplrt\Parser\Grammar\Concatenation([2]), + new \Phplrt\Parser\Grammar\Optional(140), + new \Phplrt\Parser\Grammar\Concatenation([141, 143, 4, 144, 145, 146]), + new \Phplrt\Parser\Grammar\Concatenation([33, 142]), + new \Phplrt\Parser\Grammar\Optional(34), + new \Phplrt\Parser\Grammar\Alternation([0, 63]), + new \Phplrt\Parser\Grammar\Concatenation([0]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_COLON, false), + new \Phplrt\Parser\Grammar\Concatenation([0]), + new \Phplrt\Parser\Grammar\Concatenation([63, 140]), + ], + initial: 0, + reducers: [ + 0 => $this->reduceType(...), + 3 => self::reduceNullableType(...), + 6 => $this->reduceListOrOffsetType(...), + 11 => self::reduceThisType(...), + 12 => $this->reduceLiteralType(...), + 15 => self::reduceDoubleQuotedStringLiteralType(...), + 16 => self::reduceSingleQuotedStringLiteralType(...), + 17 => self::reduceFloatLiteralType(...), + 18 => self::reduceIntLiteralType(...), + 23 => self::reduceTrueLiteralType(...), + 24 => self::reduceFalseLiteralType(...), + 25 => self::reduceNullLiteralType(...), + 26 => $this->reduceNamedType(...), + 27 => self::reduceName(...), + 30 => self::reduceIdentifier(...), + 50 => self::reduceWildcardType(...), + 55 => self::reduceCallableParameters(...), + 57 => self::reduceCallableParameter(...), + 63 => self::reduceVariable(...), + 78 => $this->reduceTemplateArguments(...), + 81 => $this->reduceTemplateArgument(...), + 83 => self::reduceIdentifierWithExtraSpace(...), + 90 => $this->reduceTemplateParameters(...), + 92 => self::reduceTemplateParameter(...), + 95 => self::reduceTemplateBoundEdge(...), + 101 => $this->reduceShapeFields(...), + 105 => self::reduceShapeFieldsList(...), + 107 => self::reduceExplicitField(...), + 116 => self::reduceImplicitField(...), + 125 => self::reduceNamespacedMask(...), + 126 => self::reduceLeadingMaskType(...), + 128 => self::reduceListOrOffsetSuffix(...), + 141 => self::reduceTernaryExpressionOperator(...), + ], + expectations: self::PARSER_EXPECTATIONS, + messages: self::PARSER_MESSAGES, + kept: self::PARSER_KEPT_TABLE, + startPrediction: self::PARSER_START_PREDICTION_TABLE, + choicePrediction: self::PARSER_CHOICE_PREDICTION_TABLE, + sequencePrediction: self::PARSER_SEQUENCE_PREDICTION_TABLE, + ); + } + + public function parse(\Phplrt\Contracts\Source\ReadableInterface $source): mixed + { + return $this->parser->parse($source); + } + + private function reduceType(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $source = $ctx->source; + $offset = $ctx->begin; + + if (\count($children) === 1) { + return $children[0]; + } + + // The suffix of a condition is the only thing the rule reads that carries + // a flag of its own, which is what the operands are told from + $condition = null; + + for ($i = 1, $count = \count($children); $i < $count; ++$i) { + if (\is_bool($children[$i])) { + $condition = \array_splice($children, $i); + + break; + } + } + + // The operands joined by an ampersand bind tighter, so they are collected + // into a group of their own, and the groups are what a union is made of + $groups = []; + $group = [$children[0]]; + $isUnion = false; + $isIntersection = false; + + for ($i = 1, $count = \count($children); $i < $count; $i += 2) { + if ($children[$i]->id === self::T_OR) { + $isUnion = true; + $groups[] = $group; + $group = [$children[$i + 1]]; + + continue; + } + + $isIntersection = true; + $group[] = $children[$i + 1]; + } + + $groups[] = $group; + + // The operand that binds tighter is the one reported first, the way the + // reading would have reached it + if ($isIntersection && $this->features->intersections === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('intersection types', $source, $offset); + } + + if ($isUnion && $this->features->unions === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('union types', $source, $offset); + } + + $statements = []; + + foreach ($groups as $group) { + $statements[] = \count($group) === 1 + ? $group[0] + : new Type\IntersectionTypeNode($group, $group[0]->offset); + } + + $statement = \count($statements) === 1 + ? $statements[0] + : new Type\UnionTypeNode($statements, $offset); + + if ($condition === null) { + return $statement; + } + + if ($this->features->conditions === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('conditional expressions', $source, $offset); + } + + $comparison = $condition[0] + ? new Type\Condition\NotEqualConditionNode($statement, $condition[1], $offset) + : new Type\Condition\EqualConditionNode($statement, $condition[1], $offset); + + return new Type\TernaryExpressionNode( + $comparison, + $condition[2], + $condition[3], + $offset, + ); + } + + private static function reduceNullableType(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\NullableTypeNode($children[0], $offset); + } + + private function reduceListOrOffsetType(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $source = $ctx->source; + $offset = $ctx->begin; + + $statement = $children[0]; + $count = \count($children); + + for ($i = 1; $i < $count; ++$i) { + $child = $children[$i]; + + switch (true) { + case $child === true: + if ($this->features->lists === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('square bracket list types', $source, $offset); + } + + $statement = new Type\TypesListNode($statement, $offset); + break; + case $child instanceof Type\TypeNode: + if ($this->features->offsets === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('type offsets', $source, $offset); + } + + $statement = new Type\TypeOffsetAccessNode($statement, $child, $offset); + break; + default: + throw Exception\InternalSemanticException::becauseSubNodeIsUnexpected( + \get_debug_type($child), + $source, + $offset, + ); + } + } + + return $statement; + } + + private static function reduceThisType(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\ThisNode($offset); + } + + private function reduceLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $source = $ctx->source; + $offset = $ctx->begin; + + if ($this->features->literals === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('literal values', $source, $offset); + } + + return $children; + } + + private static function reduceDoubleQuotedStringLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\Literal\StringLiteralNode( + StringDecoder::unpackAndDecode($children->value, true), + $children->value, + $offset, + ); + } + + private static function reduceSingleQuotedStringLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\Literal\StringLiteralNode( + StringDecoder::unpackAndDecode($children->value, false), + $children->value, + $offset, + ); + } + + private static function reduceFloatLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\Literal\FloatLiteralNode( + FloatDecoder::decode($children->value), + $children->value, + $offset, + ); + } + + private static function reduceIntLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + $decoded = IntDecoder::decode($children->value); + + return new Type\Literal\IntLiteralNode( + $decoded->value, + $children->value, + $decoded->decimal, + $offset, + ); + } + + private static function reduceTrueLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\Literal\BoolLiteralNode(true, $children->value, $offset); + } + + private static function reduceFalseLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\Literal\BoolLiteralNode(false, $children->value, $offset); + } + + private static function reduceNullLiteralType(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\Literal\NullLiteralNode( + $children->value, + $offset, + ); + } + + private function reduceNamedType(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $source = $ctx->source; + $offset = $ctx->begin; + + $head = $children[0]; + $suffix = $children[1] ?? null; + + // "Some\Any\*" and "Some\Any\SOME_*" + // + // A mask closes the type it is written at the end of, so it is read as a + // suffix of a name and never carries a suffix of its own. + if ($suffix instanceof Type\MaskNode || $suffix instanceof Type\WildcardNode) { + $parts = $head->parts; + + if ($suffix instanceof Type\MaskNode) { + // "Some\Any\*" + $mask = $suffix; + } else { + // "Some\Any\SOME_*" + $segment = \array_pop($parts); + $mask = new Type\MaskNode([$segment, ...\array_slice($children, 1)], $segment->offset); + } + + return new Type\ConstMaskNode( + $mask, + $parts === [] + ? $head->isFullyQualified + : new Type\Name($parts, $head->isFullyQualified, $head->offset), + $offset, + ); + } + + switch (true) { + // Some\Any + case $suffix === null: + return new Type\NamedTypeNode($children[0], null, null, $offset); + + // Some\Any + case $suffix instanceof Type\Template\TemplateArgumentListNode: + return new Type\NamedTypeNode($children[0], $suffix, null, $offset); + + // Some\Any{name: T, ...} + case $suffix instanceof Type\Shape\FieldsListNode: + return new Type\NamedTypeNode($children[0], $children[2] ?? null, $suffix, $offset); + + // Some\Any(T, U): V + case $suffix instanceof Type\Callable\CallableParameterListNode: + if ($this->features->callables === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $source, $offset); + } + + return new Type\CallableTypeNode($children[0], $suffix, $children[2] ?? null, null, $offset); + + // Some\Any(T): U + case $suffix instanceof Type\Template\TemplateParameterListNode: + if ($this->features->callables === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('callable types', $source, $offset); + } + + return new Type\CallableTypeNode($children[0], $children[2], $children[3] ?? null, $suffix, $offset); + + // Some\Any::CONST, Some\Any::CONST_* and Some\Any::* + default: + if (\count($children) === 3 && $children[2] instanceof Type\Identifier) { + return new Type\ClassConstNode($children[0], $children[2], $offset); + } + + return new Type\ClassConstMaskNode( + $children[0], + new Type\MaskNode(\array_slice($children, 2), $children[2]->offset), + $offset, + ); + } + } + + private static function reduceName(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + // "Some\Any" starts at a segment of its own + if ($children[0] instanceof Type\Identifier) { + return new Type\Name($children, false, $offset); + } + + // "\Some\Any" carries the separator in front of its segments, and the + // name itself is the part that follows it + $segments = \array_slice($children, 1); + + return new Type\Name($segments, true, $segments[0]->offset); + } + + private static function reduceIdentifier(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\Identifier( + $children->id === self::T_NAME_WITH_SPACE + ? \rtrim($children->value) + : $children->value, + $offset, + ); + } + + private static function reduceWildcardType(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\WildcardNode($offset); + } + + private static function reduceCallableParameters(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\Callable\CallableParameterListNode($children, $offset); + } + + private static function reduceCallableParameter(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $source = $ctx->source; + $offset = $ctx->begin; + + $parameter = new Type\Callable\CallableParameterNode( + $children[0], + null, + false, + false, + false, + $offset, + ); + + for ($i = 1, $count = \count($children); $i < $count; ++$i) { + $child = $children[$i]; + + if ($child instanceof Type\VariableNode) { + $parameter->name = $child; + + continue; + } + + switch ($child->id) { + case self::T_AMP: + $parameter->isOutput = true; + break; + + case self::T_ELLIPSIS: + $parameter->isVariadic = true; + break; + + default: + if ($parameter->isVariadic) { + throw Exception\VariadicWithDefaultException::becauseVariadicHasDefault($source, $offset); + } + + $parameter->isOptional = true; + } + } + + return $parameter; + } + + private static function reduceVariable(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\VariableNode( + new Type\Identifier(\substr($children->value, 1), $offset), + $offset, + ); + } + + private function reduceTemplateArguments(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $source = $ctx->source; + $offset = $ctx->begin; + + if ($this->features->generics === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template arguments', $source, $offset); + } + + return new Type\Template\TemplateArgumentListNode($children, $offset); + } + + private function reduceTemplateArgument(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $source = $ctx->source; + $offset = $ctx->begin; + + if (\is_array($children)) { + if ($this->features->hints === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template argument hints', $source, $offset); + } + + return new Type\Template\TemplateArgumentNode($children[1], $children[0], $offset); + } + + return new Type\Template\TemplateArgumentNode($children, null, $offset); + } + + private static function reduceIdentifierWithExtraSpace(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\Identifier(\rtrim($children->value), $offset); + } + + private function reduceTemplateParameters(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $source = $ctx->source; + $offset = $ctx->begin; + + if ($this->features->generics === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('template parameters', $source, $offset); + } + + return new Type\Template\TemplateParameterListNode($children, $offset); + } + + private static function reduceTemplateParameter(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $source = $ctx->source; + $offset = $ctx->begin; + + $upper = $lower = $default = null; + + foreach (\array_slice($children, 1) as $limit) { + if (!$limit instanceof Type\Template\TemplateBoundEdgeNode) { + if ($default !== null) { + throw Exception\TemplateBoundException::becauseBoundIsDuplicated('default', $source, $limit->offset); + } + + $default = $limit; + + continue; + } + + if ($default !== null) { + throw Exception\TemplateBoundException::becauseDefaultIsNotWrittenLast($source, $limit->operator->offset); + } + + switch ($limit->operator->value) { + case 'of': + case 'as': + if ($upper !== null) { + throw Exception\TemplateBoundException::becauseBoundIsDuplicated('upper bound', $source, $limit->offset); + } + + $upper = $limit; + break; + + case 'super': + if ($lower !== null) { + throw Exception\TemplateBoundException::becauseBoundIsDuplicated('lower bound', $source, $limit->offset); + } + + $lower = $limit; + break; + + default: + throw Exception\TemplateBoundException::becauseOperatorIsUnknown( + $limit->operator->value, + $source, + $limit->operator->offset, + ); + } + } + + return new Type\Template\TemplateParameterNode($children[0], $upper, $lower, $default, $offset); + } + + private static function reduceTemplateBoundEdge(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\Template\TemplateBoundEdgeNode($children[0], $children[1], $offset); + } + + private function reduceShapeFields(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $source = $ctx->source; + $offset = $ctx->begin; + + $count = \count($children); + + if ($count === 0) { + return [new Type\Shape\FieldsListNode([], true, $offset)]; + } + + if ($this->features->shapes === false) { + throw Exception\FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape fields', $source, $offset); + } + + $parameters = null; + + // The "..." suffix + if ($children[$count - 1] instanceof Type\Template\TemplateArgumentListNode) { + $parameters = $children[$count - 1]; + --$count; + } + + if ($children[0] instanceof Type\Shape\FieldsListNode) { + $fields = $children[0]; + $unsealed = $count > 1; + } else { + $fields = new Type\Shape\FieldsListNode([], true, $offset); + $unsealed = $count > 0; + } + + if ($unsealed) { + $fields->isSealed = false; + } + + if ($parameters === null) { + return [$fields]; + } + + return [$fields, $parameters]; + } + + private static function reduceShapeFieldsList(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $source = $ctx->source; + $offset = $ctx->begin; + + $explicit = false; + $implicit = false; + $indexed = []; + + foreach ($children as $field) { + if (!$field instanceof Type\Shape\ExplicitFieldNode) { + $implicit = true; + + continue; + } + + $explicit = true; + + if (!$field instanceof Type\Shape\SimpleFieldNodeInterface) { + continue; + } + + $key = $field->getIndex(); + + if (isset($indexed[$key])) { + throw Exception\ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated($key, $source, $field->offset); + } + + $indexed[$key] = true; + } + + if ($explicit && $implicit) { + throw Exception\ShapeKeysMixingException::becauseShapeKeysAreMixed($source, $offset); + } + + return new Type\Shape\FieldsListNode($children, true, $offset); + } + + private static function reduceExplicitField(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $source = $ctx->source; + $offset = $ctx->begin; + + $count = \count($children); + $name = $children[0]; + $value = $children[$count - 1]; + + $optional = $count === 3; + + return match (true) { + // "a", "true" and "null" + $name instanceof Type\Identifier + => new Type\Shape\NamedFieldNode($name, $value, $optional, $offset), + $name instanceof Type\Literal\IntLiteralNode, + $name instanceof Type\Literal\StringLiteralNode + => new Type\Shape\ScalarFieldNode($name, $value, $optional, $offset), + // "Some::CONST", "Some::CONST_*" and "JSON_*" + $name instanceof Type\ClassConstNode, + $name instanceof Type\ClassConstMaskNode, + $name instanceof Type\ConstMaskNode + => new Type\Shape\ComplexFieldNode($name, $value, $optional, $offset), + default => throw Exception\ShapeKeyException::becauseKeyIsNotAName($source, $name->offset), + }; + } + + private static function reduceImplicitField(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\Shape\ImplicitFieldNode($children[0], false, $offset); + } + + private static function reduceNamespacedMask(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\MaskNode($children, $offset); + } + + private static function reduceLeadingMaskType(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + // The variables below are declared by the compiler + $offset = $ctx->begin; + + return new Type\ConstMaskNode(new Type\MaskNode($children, $offset), false, $offset); + } + + private static function reduceListOrOffsetSuffix(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + return $children[0] ?? true; + } + + private static function reduceTernaryExpressionOperator(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + return \count($children) === 2; + } +} diff --git a/libs/parser/src/Internal/ExecutionContext.php b/libs/parser/src/Internal/ExecutionContext.php deleted file mode 100644 index a6a3358..0000000 --- a/libs/parser/src/Internal/ExecutionContext.php +++ /dev/null @@ -1,168 +0,0 @@ -, - * ... - * }, - * skip: list, - * grammar: array, - * reducers: array|non-empty-string, callable(Context, mixed): mixed>, - * transitions?: array - * } - * @phpstan-type ParserConfigArrayType array - * - * @internal this is an internal library class, please do not use it in your code - * @psalm-internal TypeLang\Parser - */ -final readonly class ExecutionContext -{ - /** - * @var ParserRuntime - */ - private ParserRuntime $parser; - - private LexerRuntime $lexer; - - /** - * In-memory string literal pool. - * - * @api this property is accessible inside the grammar reducers - * - * @var \WeakMap - */ - protected \WeakMap $stringPool; - - /** - * In-memory integer literal pool. - * - * @api this property is accessible inside the grammar reducers - * - * @var \WeakMap - */ - protected \WeakMap $integerPool; - - public function __construct( - /** - * @api this property is accessible inside the grammar reducers - */ - protected TypeParserFeatures $features, - /** - * @api this property is accessible inside the grammar reducers - */ - protected bool $tolerant, - ) { - /** @phpstan-var GrammarConfigArrayType $grammar */ - $grammar = require __DIR__ . '/../../resources/grammar.php'; - - $this->stringPool = new \WeakMap(); - $this->integerPool = new \WeakMap(); - - $this->lexer = $this->createLexer($grammar); - $this->parser = $this->createParser($this->lexer, $grammar); - } - - /** - * @param array|non-empty-string, callable(Context, mixed):mixed> $reducers - */ - private function createBuilder(array $reducers): BuilderInterface - { - return new NodeBuilder($reducers); - } - - /** - * @param GrammarConfigArrayType $grammar - * @return ParserRuntime - */ - private function createParser(LexerInterface $lexer, array $grammar): ParserRuntime - { - /** @var ParserRuntime */ - return new ParserRuntime( - lexer: $lexer, - grammar: $grammar['grammar'], - options: $this->createParserOptions($grammar), - ); - } - - /** - * @param GrammarConfigArrayType $grammar - * @return ParserConfigArrayType - */ - private function createParserOptions(array $grammar): array - { - return [ - ParserConfigsInterface::CONFIG_INITIAL_RULE => $grammar['initial'], - ParserConfigsInterface::CONFIG_ALLOW_TRAILING_TOKENS => $this->tolerant, - ParserConfigsInterface::CONFIG_AST_BUILDER => $this->createBuilder($grammar['reducers']), - ]; - } - - /** - * @param GrammarConfigArrayType $grammar - */ - private function createLexer(array $grammar): LexerRuntime - { - return new LexerRuntime( - tokens: $grammar['tokens']['default'], - skip: $grammar['skip'], - onUnknownToken: new PassthroughHandler(), - ); - } - - /** - * @return iterable - * @throws LexerExceptionInterface - * @throws LexerRuntimeExceptionInterface - */ - public function lex(ReadableInterface $source): iterable - { - return $this->lexer->lex($source); - } - - /** - * @throws ParserRuntimeExceptionInterface - * @throws ParserExceptionInterface - */ - public function parse(ReadableInterface $source): ?ParsedResult - { - foreach ($this->parser->parse($source) as $stmt) { - $offset = 0; - $context = $this->parser->getLastExecutionContext(); - - if ($context !== null) { - $token = $context->buffer->current(); - $offset = $token->getOffset(); - } - - return new ParsedResult($stmt, $offset); - } - - return null; - } -} diff --git a/libs/parser/src/Internal/Executor.php b/libs/parser/src/Internal/Executor.php new file mode 100644 index 0000000..71fc2e0 --- /dev/null +++ b/libs/parser/src/Internal/Executor.php @@ -0,0 +1,246 @@ + + * + * @property-read ParserRuntime $parser + */ +final class Executor extends CompiledExecutor +{ + private readonly PositionFactory $positions; + + public function __construct( + /** + * @api this property is accessible inside the grammar reducers + */ + protected readonly TypeParserFeatures $features, + ) { + $this->positions = new PositionFactory(); + + parent::__construct(); + } + + /** + * @return iterable + */ + public function lex(ReadableInterface $source): iterable + { + return $this->lexer->lex($source); + } + + /** + * Reads the source whole and returns the type it is written of. + * + * @throws ParserException in case of the source is no type of its own + */ + public function parse(ReadableInterface $source): TypeNode + { + $result = $this->build($source); + + if (!$result instanceof SuccessfulResult || $result instanceof PartialResult) { + throw $this->createError($result, $source); + } + + return $result->value; + } + + /** + * Reads as much of the source as the grammar describes and returns the + * type that part is written of. + * + * @throws ParserException in case of an internal error occurs + */ + public function partial(ReadableInterface $source): ParsedResult + { + $result = $this->build($source); + + if ($result instanceof PartialResult) { + return new PartialParsedResult( + type: $result->value, + offset: $result->token->offset, + ); + } + + if ($result instanceof SuccessfulResult) { + return new SuccessfulParsedResult($result->value, $this->length($source)); + } + + return new FailureParsedResult( + message: $this->createError($result, $source)->getMessage(), + position: $this->createPosition($source, $result->token->offset), + offset: $result->token->offset, + ); + } + + /** + * Tells whether the source is a type the grammar describes, building + * nothing of it. + * + * @throws ParserException in case of an internal error occurs + */ + public function validate(ReadableInterface $source): CheckResult + { + $result = $this->check($source); + + if ($result instanceof SuccessfulResult && !$result instanceof PartialResult) { + return new SuccessfulCheckResult(); + } + + $error = $this->createError($result, $source); + $offset = $result->token->offset; + $position = $this->createPosition($source, $offset); + + if ($result instanceof PartialResult) { + return new PartialCheckResult( + message: $error->getMessage(), + position: $position, + offset: $offset, + ); + } + + return new FailureCheckResult( + message: $error->getMessage(), + position: $position, + offset: $offset, + ); + } + + /** + * Reads the source into the type it describes. + * + * @return SuccessfulResult|FailureResult + * @throws ParserException in case of the grammar cannot be run + */ + private function build(ReadableInterface $source): SuccessfulResult|FailureResult + { + assert($this->parser instanceof ParserRuntime); + + try { + return $this->parser->analyze($source, Mode::Tolerant); + } catch (\Throwable $e) { + throw $this->raised($e, $source); + } + } + + /** + * Reads the source without building anything of it. + * + * @return SuccessfulResult|FailureResult + * @throws ParserException in case of the grammar cannot be run + */ + private function check(ReadableInterface $source): SuccessfulResult|FailureResult + { + assert($this->parser instanceof ParserRuntime); + + try { + return $this->parser->analyze($source, Mode::SyntaxCheck); + } catch (\Throwable $e) { + throw $this->raised($e, $source); + } + } + + /** + * Converts whatever the grammar raises while it reads into the error of + * this parser. + */ + private function raised(\Throwable $e, ReadableInterface $source): ParserException + { + return match (true) { + $e instanceof ParserException => $e, + $e instanceof SourceExceptionInterface + => UnreadableSourceException::becauseSourceIsUnreadable($source, $e), + default => InternalParseException::becauseInternalErrorOccurs($source, $e), + }; + } + + /** + * Returns the length of the source, which is the offset a reading that + * has stopped at nothing ends at. + * + * @return int<0, max> + * @throws ParserExceptionInterface in case of the source cannot be read + */ + private function length(ReadableInterface $source): int + { + try { + return \strlen($source->content); + } catch (\Throwable $e) { + throw $this->raised($e, $source); + } + } + + /** + * @param int<0, max> $offset + * @throws SourceExceptionInterface + */ + private function createPosition(ReadableInterface $source, int $offset): PositionInterface + { + return $this->positions->createFromOffset($source, $offset); + } + + /** + * Converts the error of the grammar into the error of this parser. + * + * @param FailureResult|PartialResult $result + */ + private function createError(FailureResult|PartialResult $result, ReadableInterface $source): ParserException + { + $error = $result->error; + + if (!$error instanceof GrammarUnexpectedTokenException) { + return UnrecognizedSyntaxException::becauseSyntaxIsUnrecognized($source, $error->token); + } + + // An input the lexer says nothing about is reported as an unrecognized + // one rather than as a token in a wrong place. + if ($error->token->channel === Channel::Unknown) { + return UnrecognizedTokenException::becauseTokenIsUnrecognized($source, $error->token); + } + + return UnexpectedTokenException::becauseTokenIsUnexpected( + message: $error->getMessage(), + source: $source, + token: $error->token, + ); + } +} diff --git a/libs/parser/src/Internal/FloatDecoder.php b/libs/parser/src/Internal/FloatDecoder.php new file mode 100644 index 0000000..cd601b3 --- /dev/null +++ b/libs/parser/src/Internal/FloatDecoder.php @@ -0,0 +1,13 @@ + \base_convert(\substr($value, 2), 16, 10), + // binary + 'b', 'B' => \base_convert(\substr($value, 2), 2, 10), + // octal + 'o', 'O' => \base_convert(\substr($value, 2), 8, 10), + // octal (legacy) + default => \base_convert($value, 8, 10), + }; + } +} diff --git a/libs/parser/src/Internal/IntDecoder/BigIntValue.php b/libs/parser/src/Internal/IntDecoder/BigIntValue.php new file mode 100644 index 0000000..3c2e2de --- /dev/null +++ b/libs/parser/src/Internal/IntDecoder/BigIntValue.php @@ -0,0 +1,22 @@ +|non-empty-string, callable(Context, mixed):mixed> $reducers - */ - public function __construct( - private array $reducers, - ) {} - - public function build(Context $context, mixed $result): mixed - { - if (!isset($this->reducers[$context->state])) { - return $result; - } - - $result = ($this->reducers[$context->state])($context, $result); - - if ($result instanceof Node && $result->offset === 0) { - $result->offset = $context->lastProcessedToken->getOffset(); - } - - return $result; - } -} diff --git a/libs/parser/src/Internal/StringDecoder.php b/libs/parser/src/Internal/StringDecoder.php new file mode 100644 index 0000000..cc0e12e --- /dev/null +++ b/libs/parser/src/Internal/StringDecoder.php @@ -0,0 +1,61 @@ + '"']); + } + + return \strtr(\substr($value, 1, -1), ["\'" => "'"]); + } + + /** + * Decodes the sequences of an unpacked body: A `\\` in a single-quoted + * one, and special chars (like a `\n`), hexadecimal (like a `\xFF`), + * octal (like a `\101`) and utf-8 (like a `\u{FFFF}`) ones in a + * double-quoted one. + */ + public static function decode(string $value, bool $isDoubleQuoted): string + { + if ($isDoubleQuoted === false) { + return \strtr($value, ['\\\\' => '\\']); + } + + if (!\str_contains($value, '\\')) { + return $value; + } + + return \strtr($value, StringSequencesFetcher::get($value)); + } + + /** + * Both of the above, in the order a literal is read in. + */ + public static function unpackAndDecode(string $value, bool $isDoubleQuoted): string + { + return StringDecoder::decode( + StringDecoder::unpack($value, $isDoubleQuoted), + $isDoubleQuoted, + ); + } +} diff --git a/libs/parser/src/Internal/StringDecoder/StringSequencesFetcher.php b/libs/parser/src/Internal/StringDecoder/StringSequencesFetcher.php new file mode 100644 index 0000000..2c441a2 --- /dev/null +++ b/libs/parser/src/Internal/StringDecoder/StringSequencesFetcher.php @@ -0,0 +1,93 @@ + + */ + private const ESCAPED_CHARS = [ + '\n' => "\n", + '\r' => "\r", + '\t' => "\t", + '\v' => "\v", + '\e' => "\e", + '\f' => "\f", + '\$' => '$', + '\\\\' => '\\', + ]; + + /** + * @var non-empty-string + */ + private const NUMERIC_PREFIX_PATTERN = '/\\\\[uxX0-7]/'; + + /** + * @var non-empty-string + */ + private const NUMERIC_SEQUENCE_PATTERN = '/\\\\(?:u\{([0-9a-fA-F]+)}|[xX]([0-9a-fA-F]{1,2})|([0-7]{1,3}))/'; + + /** + * Returns a "sequence => replacement" map of all constant sequences along + * with each hexadecimal (like a "\xFF"), octal (like a "\101") and utf-8 + * (like a "\u{FFFF}") sequence occurred in the past string. + * + * @link https://www.php.net/manual/en/language.types.string.php + * + * @return non-empty-array + */ + public static function get(string $value): array + { + if (@\preg_match(self::NUMERIC_PREFIX_PATTERN, $value) !== 1) { + return self::ESCAPED_CHARS; + } + + $count = @\preg_match_all(self::NUMERIC_SEQUENCE_PATTERN, $value, $matches, \PREG_SET_ORDER); + + if ($count === false || $count === 0) { + return self::ESCAPED_CHARS; + } + + $result = self::ESCAPED_CHARS; + + /** @var list $matches */ + foreach ($matches as $match) { + if (isset($result[$match[0]])) { + continue; + } + + // A unicode sequence, like a "\u{FFFF}" + if ($match[1] !== '') { + /** @var int<0, max> $code */ + $code = (int) \hexdec($match[1]); + + $result[$match[0]] = UtfCharRenderer::render($code); + + continue; + } + + // A hexadecimal sequence, like a "\xFF" + if (($match[2] ?? '') !== '') { + // @phpstan-ignore-next-line : A hexdec returns int<0, 255> + $result[$match[0]] = \chr((int) \hexdec($match[2] ?? '')); + + continue; + } + + // An octal sequence, like a "\101". Overflowed sequences + // (greater than a "\377") are truncated, like in PHP itself. + $result[$match[0]] = \chr(((int) \octdec($match[3] ?? '')) & 0xFF); + } + + return $result; + } +} diff --git a/libs/parser/src/Internal/StringDecoder/UtfCharRenderer.php b/libs/parser/src/Internal/StringDecoder/UtfCharRenderer.php new file mode 100644 index 0000000..416e737 --- /dev/null +++ b/libs/parser/src/Internal/StringDecoder/UtfCharRenderer.php @@ -0,0 +1,63 @@ + + */ + private const MAX_CODE_POINT = 0x10FFFF; + + /** + * @var non-empty-string + */ + private const REPLACEMENT_CHAR = "\u{FFFD}"; + + /** + * Method for encoding an utf-8 character by its code. + * + * Codes above the last Unicode code point are encoded as a replacement + * char because such a sequence cannot be expressed in utf-8. + * + * @param int<0, max> $code + */ + public static function render(int $code): string + { + if ($code > self::MAX_CODE_POINT) { + return self::REPLACEMENT_CHAR; + } + + // @phpstan-ignore-next-line : PHPStan false-positive mb_chr evaluation + if (\function_exists('\\mb_chr') && ($result = \mb_chr($code)) !== false) { + return $result; + } + + if (0x80 > $code) { + // @phpstan-ignore-next-line : Code is valid + return \chr($code); + } + + if (0x800 > $code) { + return \chr(0xC0 | $code >> 6) + . \chr(0x80 | $code & 0x3F); + } + + if (0x10000 > $code) { + return \chr(0xE0 | $code >> 12) + . \chr(0x80 | $code >> 6 & 0x3F) + . \chr(0x80 | $code & 0x3F); + } + + return \chr(0xF0 | $code >> 18) + . \chr(0x80 | $code >> 12 & 0x3F) + . \chr(0x80 | $code >> 6 & 0x3F) + . \chr(0x80 | $code & 0x3F); + } +} diff --git a/libs/parser/src/ParsedResult.php b/libs/parser/src/ParsedResult.php deleted file mode 100644 index 70862ff..0000000 --- a/libs/parser/src/ParsedResult.php +++ /dev/null @@ -1,20 +0,0 @@ - - */ - public int $offset, - ) {} -} diff --git a/libs/parser/src/Partial/FailureParsedResult.php b/libs/parser/src/Partial/FailureParsedResult.php new file mode 100644 index 0000000..7e4b202 --- /dev/null +++ b/libs/parser/src/Partial/FailureParsedResult.php @@ -0,0 +1,32 @@ + $offset + */ + public function __construct( + /** + * What stands in the way of the reading. + */ + public readonly string $message, + /** + * The line and the column the reading has stopped at. + */ + public readonly PositionInterface $position, + /** + * The offset the reading has stopped at, which is the one the + * {@see $position} above is made of. + */ + public readonly int $offset, + ) {} +} diff --git a/libs/parser/src/Partial/ParsedResult.php b/libs/parser/src/Partial/ParsedResult.php new file mode 100644 index 0000000..3032bfa --- /dev/null +++ b/libs/parser/src/Partial/ParsedResult.php @@ -0,0 +1,18 @@ +partial($source); + * + * echo \substr($source, $result->offset); + * // => " and more" + * ``` + */ +final class PartialParsedResult extends SuccessfulParsedResult {} diff --git a/libs/parser/src/Partial/SuccessfulParsedResult.php b/libs/parser/src/Partial/SuccessfulParsedResult.php new file mode 100644 index 0000000..0e4c73b --- /dev/null +++ b/libs/parser/src/Partial/SuccessfulParsedResult.php @@ -0,0 +1,33 @@ + $offset + */ + public function __construct( + /** + * The type the source is read into. + */ + public readonly TypeNode $type, + /** + * The offset the reading has stopped at. + */ + public readonly int $offset, + ) {} +} diff --git a/libs/parser/src/Traverser.php b/libs/parser/src/Traverser.php index 6a728d3..755eb75 100644 --- a/libs/parser/src/Traverser.php +++ b/libs/parser/src/Traverser.php @@ -23,7 +23,9 @@ public function __construct( iterable $visitors = [], private PropertyAccessorInterface $propertyAccessor = new SimplePropertyAccessor(), ) { - $this->visitors = \iterator_to_array($visitors, false); + $this->visitors = \is_array($visitors) + ? \array_values($visitors) + : \iterator_to_array($visitors, false); } /** diff --git a/libs/parser/src/Traverser/DumperVisitor.php b/libs/parser/src/Traverser/DumperVisitor.php index 8d0131a..4af86b3 100644 --- a/libs/parser/src/Traverser/DumperVisitor.php +++ b/libs/parser/src/Traverser/DumperVisitor.php @@ -11,7 +11,7 @@ abstract class DumperVisitor extends Visitor /** * @var non-empty-string */ - public const string DEFAULT_SIMPLIFIED_NODE_NAMESPACE = 'TypeLang\\Type\\'; + public const DEFAULT_SIMPLIFIED_NODE_NAMESPACE = 'TypeLang\\Type\\'; /** * @var int<0, max> @@ -35,7 +35,9 @@ public function enter(Node $node): ?Command $suffix = \str_replace($this->simplifyNodeNamespace, '', $node::class); if ($node instanceof \Stringable) { - $suffix .= \sprintf('(%s)', (string) $node); + $suffix .= $this->printStringableNodeSuffix($node); + } else { + $suffix .= $this->printNodePropertiesSuffix($node); } $this->write($prefix . $suffix . "\n"); @@ -43,6 +45,47 @@ public function enter(Node $node): ?Command return null; } + /** + * @return non-empty-string + */ + private function printStringableNodeSuffix(\Stringable $node): string + { + return \sprintf('(%s)', (string) $node); + } + + /** + * Returns a "(prop=value, ...)" suffix built from the writable scalar + * properties of the node, or an empty string in case of there are none. + */ + private function printNodePropertiesSuffix(Node $node): string + { + $result = []; + + $reflection = new \ReflectionObject($node); + + foreach ($reflection->getProperties() as $property) { + // Skip readonly + static and builtin "offset" properties + if ($property->isStatic() || $property->isReadOnly() || $property->getName() === 'offset') { + continue; + } + + $value = $property->getValue($node); + + // Skip non-scalar properties + if (!\is_scalar($value)) { + continue; + } + + $result[] = \sprintf('%s=%s', $property->getName(), \var_export($value, true)); + } + + if ($result === []) { + return ''; + } + + return \sprintf('(%s)', \implode(', ', $result)); + } + public function leave(Node $node): void { // @phpstan-ignore-next-line : $depth is always non-negative diff --git a/libs/parser/src/Traverser/MatcherVisitor.php b/libs/parser/src/Traverser/MatcherVisitor.php index 9dba466..37e7e5d 100644 --- a/libs/parser/src/Traverser/MatcherVisitor.php +++ b/libs/parser/src/Traverser/MatcherVisitor.php @@ -8,11 +8,7 @@ class MatcherVisitor extends Visitor { - public private(set) ?Node $node = null; - - public bool $isFound { - get => $this->node !== null; - } + public ?Node $node = null; private bool $shouldContinue = false; @@ -25,9 +21,18 @@ public function __construct( private readonly ?\Closure $break = null, ) {} + /** + * Returns {@see true} in case of a node matching the criteria was found. + */ + public function hasMatches(): bool + { + return $this->node !== null; + } + public function before(): void { $this->node = null; + $this->shouldContinue = false; } public function enter(Node $node): ?Command diff --git a/libs/parser/src/Traverser/PropertyAccessor/SimplePropertyAccessor.php b/libs/parser/src/Traverser/PropertyAccessor/SimplePropertyAccessor.php index 685184e..e16081c 100644 --- a/libs/parser/src/Traverser/PropertyAccessor/SimplePropertyAccessor.php +++ b/libs/parser/src/Traverser/PropertyAccessor/SimplePropertyAccessor.php @@ -4,15 +4,20 @@ namespace TypeLang\Parser\Traverser\PropertyAccessor; -final readonly class SimplePropertyAccessor implements PropertyAccessorInterface +final class SimplePropertyAccessor implements PropertyAccessorInterface { /** * Skips static and hooked properties */ private function shouldSkip(\ReflectionProperty $property): bool { - return $property->isStatic() - || $property->hasHooks(); + if ($property->isStatic() || $property->isReadOnly()) { + return true; + } + + // Property hooks are available since PHP 8.4 + return \method_exists($property, 'hasHooks') + && $property->hasHooks(); } public function unwrap(object $object): iterable diff --git a/libs/parser/src/Traverser/StreamDumperVisitor.php b/libs/parser/src/Traverser/StreamDumperVisitor.php index 489ba98..1340265 100644 --- a/libs/parser/src/Traverser/StreamDumperVisitor.php +++ b/libs/parser/src/Traverser/StreamDumperVisitor.php @@ -6,7 +6,7 @@ final class StreamDumperVisitor extends DumperVisitor { - public const string DEFAULT_OUTPUT_STREAM = 'php://stderr'; + public const DEFAULT_OUTPUT_STREAM = 'php://stderr'; /** * @var resource diff --git a/libs/parser/src/Traverser/StringDumperVisitor.php b/libs/parser/src/Traverser/StringDumperVisitor.php index b336e74..4ca6951 100644 --- a/libs/parser/src/Traverser/StringDumperVisitor.php +++ b/libs/parser/src/Traverser/StringDumperVisitor.php @@ -6,7 +6,7 @@ final class StringDumperVisitor extends DumperVisitor { - public private(set) string $output = ''; + public string $output = ''; public function before(): void { diff --git a/libs/parser/src/Traverser/TypeMapVisitor.php b/libs/parser/src/Traverser/TypeMapVisitor.php index 76cc0ff..a15be2d 100644 --- a/libs/parser/src/Traverser/TypeMapVisitor.php +++ b/libs/parser/src/Traverser/TypeMapVisitor.php @@ -37,11 +37,19 @@ public function enter(Node $node): ?Command switch (true) { case $node instanceof NamedTypeNode: case $node instanceof CallableTypeNode: - case $node instanceof ConstMaskNode: $node->name = $this->map($node->name); return null; + case $node instanceof ConstMaskNode: + // A mask carries no namespace of its own in case of the + // constant is written with none, and nothing is mapped then + if ($node->namespaceOrFullyQualified instanceof Name) { + $node->namespaceOrFullyQualified = $this->map($node->namespaceOrFullyQualified); + } + + return null; + case $node instanceof ClassConstNode: case $node instanceof ClassConstMaskNode: $node->class = $this->map($node->class); diff --git a/libs/parser/src/TypeParser.php b/libs/parser/src/TypeParser.php index d17fbdc..dadb5fe 100644 --- a/libs/parser/src/TypeParser.php +++ b/libs/parser/src/TypeParser.php @@ -5,37 +5,34 @@ namespace TypeLang\Parser; use JetBrains\PhpStorm\Language; -use Phplrt\Contracts\Parser\ParserRuntimeExceptionInterface; +use Phplrt\Contracts\Lexer\TokenInterface; +use Phplrt\Contracts\Source\Exception\SourceExceptionInterface; use Phplrt\Contracts\Source\ReadableInterface; -use Phplrt\Contracts\Source\SourceExceptionInterface; use Phplrt\Contracts\Source\SourceFactoryInterface; -use Phplrt\Parser\Exception\UnexpectedTokenException as LexerUnexpectedTokenException; -use Phplrt\Parser\Exception\UnrecognizedTokenException as LexerUnrecognizedTokenException; use Phplrt\Source\SourceFactory; -use TypeLang\Parser\Exception\InternalParseException; -use TypeLang\Parser\Exception\ParseException; -use TypeLang\Parser\Exception\SemanticException; -use TypeLang\Parser\Exception\SemanticParseException; -use TypeLang\Parser\Exception\UnexpectedTokenException; -use TypeLang\Parser\Exception\UnrecognizedSyntaxException; -use TypeLang\Parser\Exception\UnrecognizedTokenException; -use TypeLang\Parser\Internal\ExecutionContext; +use TypeLang\Parser\Exception\ParserExceptionInterface; +use TypeLang\Parser\Internal\Executor; +use TypeLang\Parser\Partial\ParsedResult; +use TypeLang\Parser\Validation\CheckResult; use TypeLang\Type\TypeNode; +/** + * @template-contravariant TSource of mixed = mixed + * + * @template-implements TypeParserInterface + */ final class TypeParser implements TypeParserInterface { - private ExecutionContext $strict { - get => $this->strict ??= new ExecutionContext($this->features, false); - } + private readonly SourceFactoryInterface $sources; - private ExecutionContext $tolerant { - get => $this->tolerant ??= new ExecutionContext($this->features, true); - } + private ?Executor $executor = null; public function __construct( public readonly TypeParserFeatures $features = new TypeParserFeatures(), - private readonly SourceFactoryInterface $sources = new SourceFactory(), - ) {} + ?SourceFactoryInterface $sources = null, + ) { + $this->sources = $sources ?? SourceFactory::createDefault(); + } /** * Returns a new parser with an overridden parser feature flag. @@ -55,99 +52,72 @@ public function withFeatures(bool ...$features): self ); } - public function parse(#[Language('PHP')] mixed $source): TypeNode + /** + * Reads the provided source code into the tokens it is written of, + * building nothing out of them. + * + * The tokens are what every other method of this parser reads the source + * through, so this is the source as the grammar sees it: what a token is + * called, what it carries, and where it stands. + * + * ``` + * foreach ($parser->lex('array{ field: result }') as $token) { + * echo $token->name . ' ' . $token->value . \PHP_EOL; + * } + * + * // => T_NAME array + * // => T_BRACE_OPEN { + * // => ... + * ``` + * + * @param TSource $source source code to read + * @return iterable the tokens the source is + * written of + * @throws \Throwable in case of internal error occurs + */ + public function lex(#[Language('PHP')] mixed $source): iterable { - $result = $this->execute($this->strict, $source); + $executor = $this->getExecutor(); - return $result->type; + return $executor->lex($this->toSource($source)); } - public function parseTolerant(#[Language('PHP')] mixed $source): ParsedResult + public function parse(#[Language('PHP')] mixed $source): TypeNode { - return $this->execute($this->tolerant, $source); - } + $executor = $this->getExecutor(); - private function execute(ExecutionContext $context, mixed $source): ParsedResult - { - try { - $instance = $this->sources->create($source); - - try { - return $context->parse($instance) - ?? throw InternalParseException::becauseTypeStatementIsUnreadable(); - } catch (LexerUnexpectedTokenException $e) { - throw $this->unexpectedTokenError($e, $instance); - } catch (LexerUnrecognizedTokenException $e) { - throw $this->unrecognizedTokenError($e, $instance); - } catch (ParserRuntimeExceptionInterface $e) { - throw $this->parserRuntimeError($e, $instance); - } catch (SemanticException $e) { - throw $this->semanticError($e, $instance); - } catch (\Throwable $e) { - throw $this->internalError($e, $instance); - } - } catch (SourceExceptionInterface $e) { - throw InternalParseException::becauseSourceIsUnreadable($e); - } + return $executor->parse($this->toSource($source)); } - /** - * @throws SourceExceptionInterface in case of source content reading error - */ - private function unexpectedTokenError(LexerUnexpectedTokenException $e, ReadableInterface $source): ParseException + public function partial(#[Language('PHP')] mixed $source): ParsedResult { - $token = $e->getToken(); + $executor = $this->getExecutor(); - return UnexpectedTokenException::becauseTokenIsUnexpected( - token: $token->getValue(), - statement: $source->getContents(), - offset: $token->getOffset(), - ); + return $executor->partial($this->toSource($source)); } - /** - * @throws SourceExceptionInterface in case of source content reading error - */ - private function unrecognizedTokenError(LexerUnrecognizedTokenException $e, ReadableInterface $source): ParseException + public function validate(#[Language('PHP')] mixed $source): CheckResult { - $token = $e->getToken(); - - return UnrecognizedTokenException::becauseTokenIsUnrecognized( - token: $token->getValue(), - statement: $source->getContents(), - offset: $token->getOffset(), - ); - } + $executor = $this->getExecutor(); - /** - * @throws SourceExceptionInterface in case of source content reading error - */ - private function semanticError(SemanticException $e, ReadableInterface $source): ParseException - { - return SemanticParseException::becauseSemanticErrorOccurs($e, $source); + return $executor->validate($this->toSource($source)); } /** - * @throws SourceExceptionInterface in case of source content reading error + * @throws SourceExceptionInterface in case of no source can be created out + * of the given value */ - private function parserRuntimeError(ParserRuntimeExceptionInterface $e, ReadableInterface $source): ParseException + private function toSource(mixed $source): ReadableInterface { - $token = $e->getToken(); - - return UnrecognizedSyntaxException::becauseSyntaxIsUnrecognized( - statement: $source->getContents(), - offset: $token->getOffset(), - ); + return $this->sources->create($source); } /** - * @throws SourceExceptionInterface in case of source content reading error + * Returns a lazily created parser recognizing a source with the features + * of this one. */ - private function internalError(\Throwable $e, ReadableInterface $source): ParseException + private function getExecutor(): Executor { - return InternalParseException::becauseInternalErrorOccurs( - statement: $source->getContents(), - e: $e, - ); + return $this->executor ??= new Executor($this->features); } } diff --git a/libs/parser/src/TypeParserFeatures.php b/libs/parser/src/TypeParserFeatures.php index 2c3dc3d..8c87aca 100644 --- a/libs/parser/src/TypeParserFeatures.php +++ b/libs/parser/src/TypeParserFeatures.php @@ -20,65 +20,60 @@ * // => Error: Conditional expressions not allowed in ... * ``` */ -final readonly class TypeParserFeatures +final class TypeParserFeatures { - public const bool CONDITIONAL_FEATURES_DEFAULT_VALUE = true; - public const bool SHAPES_FEATURES_DEFAULT_VALUE = true; - public const bool CALLABLES_FEATURES_DEFAULT_VALUE = true; - public const bool LITERALS_FEATURES_DEFAULT_VALUE = true; - public const bool GENERICS_FEATURES_DEFAULT_VALUE = true; - public const bool UNION_FEATURES_DEFAULT_VALUE = true; - public const bool INTERSECTION_FEATURES_DEFAULT_VALUE = true; - public const bool LIST_FEATURES_DEFAULT_VALUE = true; - public const bool OFFSETS_FEATURES_DEFAULT_VALUE = true; - public const bool HINTS_FEATURES_DEFAULT_VALUE = true; - public const bool ATTRIBUTES_FEATURES_DEFAULT_VALUE = true; + public const CONDITIONAL_FEATURES_DEFAULT_VALUE = true; + public const SHAPES_FEATURES_DEFAULT_VALUE = true; + public const CALLABLES_FEATURES_DEFAULT_VALUE = true; + public const LITERALS_FEATURES_DEFAULT_VALUE = true; + public const GENERICS_FEATURES_DEFAULT_VALUE = true; + public const UNION_FEATURES_DEFAULT_VALUE = true; + public const INTERSECTION_FEATURES_DEFAULT_VALUE = true; + public const LIST_FEATURES_DEFAULT_VALUE = true; + public const OFFSETS_FEATURES_DEFAULT_VALUE = true; + public const HINTS_FEATURES_DEFAULT_VALUE = true; public function __construct( /** * Enables or disables support for conditional types such as `T ? U : V` */ - public bool $conditions = self::CONDITIONAL_FEATURES_DEFAULT_VALUE, + public readonly bool $conditions = self::CONDITIONAL_FEATURES_DEFAULT_VALUE, /** * Enables or disables support for shape types such as `T{key: U}` */ - public bool $shapes = self::SHAPES_FEATURES_DEFAULT_VALUE, + public readonly bool $shapes = self::SHAPES_FEATURES_DEFAULT_VALUE, /** * Enables or disables support for callable types such as `fn(T, U): V` */ - public bool $callables = self::CALLABLES_FEATURES_DEFAULT_VALUE, + public readonly bool $callables = self::CALLABLES_FEATURES_DEFAULT_VALUE, /** * Enables or disables support for literal types such as `42` or `'foo'` */ - public bool $literals = self::LITERALS_FEATURES_DEFAULT_VALUE, + public readonly bool $literals = self::LITERALS_FEATURES_DEFAULT_VALUE, /** * Enables or disables support for generic type arguments such as `T` */ - public bool $generics = self::GENERICS_FEATURES_DEFAULT_VALUE, + public readonly bool $generics = self::GENERICS_FEATURES_DEFAULT_VALUE, /** * Enables or disables support for union types such as `T | U` */ - public bool $unions = self::UNION_FEATURES_DEFAULT_VALUE, + public readonly bool $unions = self::UNION_FEATURES_DEFAULT_VALUE, /** * Enables or disables support for intersection types such as `T & U` */ - public bool $intersections = self::INTERSECTION_FEATURES_DEFAULT_VALUE, + public readonly bool $intersections = self::INTERSECTION_FEATURES_DEFAULT_VALUE, /** * Enables or disables support for short list types such as `T[]` */ - public bool $lists = self::LIST_FEATURES_DEFAULT_VALUE, + public readonly bool $lists = self::LIST_FEATURES_DEFAULT_VALUE, /** * Enables or disables support for offset access types such as `T[U]` */ - public bool $offsets = self::OFFSETS_FEATURES_DEFAULT_VALUE, + public readonly bool $offsets = self::OFFSETS_FEATURES_DEFAULT_VALUE, /** * Enables or disables support for generic variance hints such as `T` */ - public bool $hints = self::HINTS_FEATURES_DEFAULT_VALUE, - /** - * Enables or disables support for attributes such as `#[attr]` - */ - public bool $attributes = self::ATTRIBUTES_FEATURES_DEFAULT_VALUE, + public readonly bool $hints = self::HINTS_FEATURES_DEFAULT_VALUE, ) {} /** diff --git a/libs/parser/src/TypeParserInterface.php b/libs/parser/src/TypeParserInterface.php index b2163cc..2b9d99f 100644 --- a/libs/parser/src/TypeParserInterface.php +++ b/libs/parser/src/TypeParserInterface.php @@ -6,6 +6,8 @@ use JetBrains\PhpStorm\Language; use TypeLang\Parser\Exception\ParserExceptionInterface; +use TypeLang\Parser\Partial\ParsedResult; +use TypeLang\Parser\Validation\CheckResult; use TypeLang\Type\TypeNode; /** @@ -49,7 +51,7 @@ public function parse(#[Language('PHP')] mixed $source): TypeNode; * code. * * ``` - * $result = $parser->tolerant('array{ field: result } This is an example'); + * $result = $parser->partial('array{ field: result } This is an example'); * * $result->type; * // => NamedTypeNode{ name: "array", ... @@ -61,7 +63,7 @@ public function parse(#[Language('PHP')] mixed $source): TypeNode; * To get information about the remaining part, just apply `substr` to source: * ``` * $source = 'array{ field: result } This is an example'; - * $result = $parser->tolerant($source); + * $result = $parser->partial($source); * * echo substr($source, $result->offset); * // => "This is an example" @@ -73,5 +75,31 @@ public function parse(#[Language('PHP')] mixed $source): TypeNode; * @throws ParserExceptionInterface in case of parsing exception occurs * @throws \Throwable in case of internal error occurs */ - public function parseTolerant(#[Language('PHP')] mixed $source): ParsedResult; + public function partial(#[Language('PHP')] mixed $source): ParsedResult; + + /** + * Tells whether the provided source code is a type, building nothing + * of it. + * + * Nothing is built while a source is checked, so this method is the + * cheaper one of the three whenever the type itself is of no use. + * + * ``` + * $parser->validate('array{ field: result }'); + * // => SuccessfulCheckResult + * + * $parser->validate('array{ field: result } and more'); + * // => PartialCheckResult + * + * $parser->validate('array{'); + * // => FailureCheckResult + * ``` + * + * @param TSource $source source code to check + * @return CheckResult what stands in the way of the reading, in case of + * anything does + * @throws ParserExceptionInterface in case of parsing exception occurs + * @throws \Throwable in case of internal error occurs + */ + public function validate(#[Language('PHP')] mixed $source): CheckResult; } diff --git a/libs/parser/src/TypeResolver.php b/libs/parser/src/TypeResolver.php index 687c994..13d0a40 100644 --- a/libs/parser/src/TypeResolver.php +++ b/libs/parser/src/TypeResolver.php @@ -9,13 +9,13 @@ use TypeLang\Parser\TypeResolver\PhpUseStatementsTransformer; use TypeLang\Type\TypeNode; -final readonly class TypeResolver +final class TypeResolver { public function __construct( /** * @var array */ - private array $imports = [], + private readonly array $imports = [], ) {} /** @@ -116,8 +116,8 @@ public function withTypeImportAs(string $name, string $alias): self */ public function withTypeImportsFromClass(\ReflectionClass $class): self { - $statements = new PhpUseStatementsReader() - ->getClassUseStatements($class); + $reader = new PhpUseStatementsReader(); + $statements = $reader->getClassUseStatements($class); return new self([...$this->imports, ...$statements]); } @@ -162,8 +162,8 @@ public function withTypeImportsFromFunction(\ReflectionFunctionAbstract $functio return $this->withTypeImportsFromClass($function->getDeclaringClass()); } - $statements = new PhpUseStatementsReader() - ->getFunctionUseStatements($function); + $reader = new PhpUseStatementsReader(); + $statements = $reader->getFunctionUseStatements($function); return new self([...$this->imports, ...$statements]); } diff --git a/libs/parser/src/TypeResolver/PhpUseStatementsReader.php b/libs/parser/src/TypeResolver/PhpUseStatementsReader.php index 4b72cb2..1f82480 100644 --- a/libs/parser/src/TypeResolver/PhpUseStatementsReader.php +++ b/libs/parser/src/TypeResolver/PhpUseStatementsReader.php @@ -7,10 +7,10 @@ use TypeLang\Parser\TypeResolver\PhpUseStatementsReader\NamespaceFinder; use TypeLang\Parser\TypeResolver\PhpUseStatementsReader\ReflectionSourcePrefixReader; -final readonly class PhpUseStatementsReader +final class PhpUseStatementsReader { - private ReflectionSourcePrefixReader $reader; - private NamespaceFinder $namespace; + private readonly ReflectionSourcePrefixReader $reader; + private readonly NamespaceFinder $namespace; public function __construct() { @@ -48,7 +48,23 @@ public function getFunctionUseStatements(\ReflectionFunctionAbstract $function): $header = ''; } - return [...$this->parse($function->getNamespaceName(), $header)]; + return [...$this->parse($this->getFunctionNamespace($function, $header), $header)]; + } + + /** + * Returns the namespace the given function is written in. + */ + private function getFunctionNamespace(\ReflectionFunctionAbstract $function, string $header): string + { + if (\PHP_VERSION_ID < 80400) { + return $function->getNamespaceName(); + } + + if ($function->isClosure()) { + return $this->namespace->findLast($this->lex($header)); + } + + return $function->getNamespaceName(); } /** diff --git a/libs/parser/src/TypeResolver/PhpUseStatementsReader/NamespaceFinder.php b/libs/parser/src/TypeResolver/PhpUseStatementsReader/NamespaceFinder.php index e4b7283..772cdd0 100644 --- a/libs/parser/src/TypeResolver/PhpUseStatementsReader/NamespaceFinder.php +++ b/libs/parser/src/TypeResolver/PhpUseStatementsReader/NamespaceFinder.php @@ -4,8 +4,31 @@ namespace TypeLang\Parser\TypeResolver\PhpUseStatementsReader; -final readonly class NamespaceFinder +final class NamespaceFinder { + /** + * Returns the last namespace the given source declares, which is the one + * whatever the source ends with is written in. + * + * @param \Iterator $tokens + */ + public function findLast(\Iterator $tokens): string + { + $result = ''; + + while ($tokens->valid()) { + if ($tokens->current()->id === \T_NAMESPACE) { + $result = $this->readNamespace($tokens); + + continue; + } + + $tokens->next(); + } + + return $result; + } + /** * @param \Iterator $tokens * @return \Iterator diff --git a/libs/parser/src/TypeResolver/PhpUseStatementsReader/ReflectionSourcePrefixReader.php b/libs/parser/src/TypeResolver/PhpUseStatementsReader/ReflectionSourcePrefixReader.php index dfce2d1..186d7d5 100644 --- a/libs/parser/src/TypeResolver/PhpUseStatementsReader/ReflectionSourcePrefixReader.php +++ b/libs/parser/src/TypeResolver/PhpUseStatementsReader/ReflectionSourcePrefixReader.php @@ -4,7 +4,7 @@ namespace TypeLang\Parser\TypeResolver\PhpUseStatementsReader; -final readonly class ReflectionSourcePrefixReader +final class ReflectionSourcePrefixReader { /** * @param \ReflectionClass $class diff --git a/libs/parser/src/TypeResolver/PhpUseStatementsTransformer.php b/libs/parser/src/TypeResolver/PhpUseStatementsTransformer.php index 5a2ae3a..c8da302 100644 --- a/libs/parser/src/TypeResolver/PhpUseStatementsTransformer.php +++ b/libs/parser/src/TypeResolver/PhpUseStatementsTransformer.php @@ -41,12 +41,12 @@ * // > } * ``` */ -final readonly class PhpUseStatementsTransformer +final class PhpUseStatementsTransformer { /** * @var array */ - private array $replacements; + private readonly array $replacements; /** * @param iterable $replacements @@ -73,7 +73,7 @@ private function format(iterable $replacements): array // normalize key if (\is_int($key) || $key === '') { - $key = $replacement->last->toString(); + $key = $replacement->getLastPartAsString(); } $result[\strtolower($key)] = $replacement; @@ -89,17 +89,17 @@ private static function isNonSubstituted(Name $name): bool return true; } + $first = $name->getFirstPart(); + // Non-FQN names prefixed with "namespace" must not be replaced - if (!$name->isSimple) { - return $name->first->toLowerString() === 'namespace'; + if (!$name->isSimple()) { + return $first->toLowerString() === 'namespace'; } - $first = $name->first; - // builtin types must not be replaced - return $first->isBuiltin + return $first->isBuiltin() // special types also must not be replaced - || $first->isSpecial; + || $first->isSpecial(); } public function __invoke(Name $name): ?Name @@ -108,7 +108,7 @@ public function __invoke(Name $name): ?Name return null; } - $first = \strtolower($name->first->toString()); + $first = \strtolower($name->getFirstPartAsString()); $prefix = $this->replacements[$first] ?? null; return $prefix?->mergeWith($name); diff --git a/libs/parser/src/Validation/CheckResult.php b/libs/parser/src/Validation/CheckResult.php new file mode 100644 index 0000000..7cf9923 --- /dev/null +++ b/libs/parser/src/Validation/CheckResult.php @@ -0,0 +1,12 @@ + $offset + */ + public function __construct( + /** + * What stands in the way of the reading. + */ + public readonly string $message, + /** + * The line and the column the reading has stopped at. + */ + public readonly PositionInterface $position, + /** + * The offset the reading has stopped at, which is the one the + * {@see $position} above is made of. + */ + public readonly int $offset, + ) {} +} diff --git a/libs/parser/src/Validation/PartialCheckResult.php b/libs/parser/src/Validation/PartialCheckResult.php new file mode 100644 index 0000000..347eb16 --- /dev/null +++ b/libs/parser/src/Validation/PartialCheckResult.php @@ -0,0 +1,11 @@ +create($statement); + } + + /** + * @param int<0, max> $offset + * @param non-empty-string|null $name + */ + private static function token( + string $value, + int $offset = 0, + ?string $name = 'T_NAME', + ChannelInterface $channel = Channel::Default, + ): TokenInterface { + return new Token( + id: 0, + name: $name, + channel: $channel, + value: $value, + offset: $offset, + ); + } + + private static function unexpected(string $statement, string $message = 'Syntax error'): UnexpectedTokenException + { + return UnexpectedTokenException::becauseTokenIsUnexpected( + message: $message, + source: self::source($statement), + token: self::token('foo', 4), + ); + } + + #[Test] + public function everyParseExceptionIsALogicError(): void + { + $exception = InternalParseException::becauseInternalErrorOccurs( + self::source('int'), + new \LogicException(), + ); + + self::assertInstanceOf(ParserException::class, $exception); + self::assertInstanceOf(\LogicException::class, $exception); + } + + #[Test] + public function theMessageCarriesTheSourceButNoPlace(): void + { + $source = self::source('int|foo'); + $token = self::token('foo', 4); + + $exception = UnexpectedTokenException::becauseTokenIsUnexpected( + message: 'Syntax error, unexpected "foo" (T_NAME)', + source: $source, + token: $token, + ); + + self::assertSame( + 'Syntax error, unexpected "foo" (T_NAME) in "int|foo"', + $exception->getMessage(), + ); + self::assertSame($source, $exception->source); + self::assertSame($token, $exception->token); + } + + #[Test] + public function theRenderedErrorCarriesTheLocation(): void + { + $exception = self::unexpected('int|foo'); + + self::assertStringContainsString( + 'Syntax error in "int|foo" on line 1 at column 5', + (string) $exception, + ); + } + + #[Test] + public function thePrintedErrorIsTheOnePhpPrints(): void + { + $exception = self::unexpected('int|foo'); + + $printed = (string) $exception; + + self::assertStringStartsWith(UnexpectedTokenException::class, $printed); + self::assertStringContainsString('Syntax error in "int|foo"', $printed); + self::assertStringContainsString('Stack trace:', $printed); + self::assertStringContainsString($exception->getFile(), $printed); + } + + #[Test] + public function thePrintedErrorLeavesTheMessageAlone(): void + { + $exception = self::unexpected('int|foo'); + + $printed = (string) $exception; + + self::assertSame('Syntax error in "int|foo"', $exception->getMessage()); + self::assertSame($printed, (string) $exception); + } + + #[Test] + public function theMultilineStatementIsReportedUsingTheLineAndColumn(): void + { + $exception = UnexpectedTokenException::becauseTokenIsUnexpected( + message: 'Syntax error', + source: self::source("int|\nx"), + token: self::token('x', 5), + ); + + self::assertStringContainsString('on line 2 at column 1', (string) $exception); + } + + #[Test] + public function theLongStatementIsTruncated(): void + { + $statement = \str_repeat('x', 100) . 'foo'; + $exception = self::unexpected($statement); + + self::assertStringContainsString('…', (string) $exception); + self::assertStringNotContainsString($statement, (string) $exception); + } + + #[Test] + public function theUnrecognizedTokenIsReportedByItsValue(): void + { + $exception = UnrecognizedTokenException::becauseTokenIsUnrecognized( + self::source('int|%'), + self::token('%', 4, null, Channel::Unknown), + ); + + self::assertSame( + 'Syntax error, unexpected "%" (unknown token) in "int|%"', + $exception->getMessage(), + ); + self::assertStringContainsString('on line 1 at column 5', (string) $exception); + } + + #[Test] + public function theEndOfInputIsReportedInsteadOfAToken(): void + { + $exception = UnrecognizedTokenException::becauseTokenIsUnrecognized( + self::source('int|'), + self::token('', 4, null, Channel::EndOfInput), + ); + + self::assertStringStartsWith('Syntax error, unexpected end of input', $exception->getMessage()); + } + + #[Test] + public function theUnrecognizedSyntaxIsReportedWithItsLocation(): void + { + $exception = UnrecognizedSyntaxException::becauseSyntaxIsUnrecognized( + self::source('int|'), + self::token('', 4, null, Channel::EndOfInput), + ); + + self::assertSame('Internal syntax error in "int|"', $exception->getMessage()); + self::assertStringContainsString('on line 1 at column 5', (string) $exception); + } + + #[Test] + public function theBlankStatementIsReportedAsEmpty(): void + { + $exception = UnrecognizedSyntaxException::becauseSyntaxIsUnrecognized( + self::source(' '), + self::token('', 0, null, Channel::EndOfInput), + ); + + self::assertStringContainsString('', (string) $exception); + } + + #[Test] + public function theInternalErrorKeepsThePreviousException(): void + { + $previous = new \LogicException('oops'); + $exception = InternalParseException::becauseInternalErrorOccurs(self::source('int'), $previous); + + self::assertSame('An internal error occurred while parsing "int"', $exception->getMessage()); + self::assertSame($previous, $exception->getPrevious()); + } + + #[Test] + public function theUnreadableSourceIsReportedUsingTheOriginalMessage(): void + { + $previous = new class ('source is unreadable') extends \RuntimeException implements + \Phplrt\Contracts\Source\Exception\SourceExceptionInterface {}; + + $source = self::source('int'); + $exception = UnreadableSourceException::becauseSourceIsUnreadable($source, $previous); + + self::assertInstanceOf(ParserException::class, $exception); + self::assertSame('source is unreadable', $exception->getMessage()); + self::assertSame($source, $exception->source); + self::assertSame($previous, $exception->getPrevious()); + } + + /** + * Every error is told apart by the class it is of, so none of them carries + * a code. + */ + #[Test] + public function everyErrorIsToldApartByItsClassAlone(): void + { + self::assertSame(0, self::unexpected('int|foo')->getCode()); + self::assertSame(0, InternalParseException::becauseInternalErrorOccurs( + self::source('int'), + new \LogicException(), + )->getCode()); + } +} diff --git a/libs/parser/tests/Exception/SemanticExceptionTest.php b/libs/parser/tests/Exception/SemanticExceptionTest.php new file mode 100644 index 0000000..9c403f2 --- /dev/null +++ b/libs/parser/tests/Exception/SemanticExceptionTest.php @@ -0,0 +1,177 @@ +create($statement); + } + + /** + * @return iterable + */ + public static function factoryDataProvider(): iterable + { + yield 'feature not allowed' => [ + static fn(ReadableInterface $source, int $offset): SemanticException + => FeatureNotAllowedException::becauseFeatureIsNotAllowed('shapes', $source, $offset), + ]; + + yield 'unexpected sub-node' => [ + static fn(ReadableInterface $source, int $offset): SemanticException + => InternalSemanticException::becauseSubNodeIsUnexpected('Example', $source, $offset), + ]; + + yield 'invalid conditional operator' => [ + static fn(ReadableInterface $source, int $offset): SemanticException + => InvalidConditionalOperatorException::becauseConditionalOperatorIsInvalid('~', $source, $offset), + ]; + + yield 'shape field duplication' => [ + static fn(ReadableInterface $source, int $offset): SemanticException + => ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated('key', $source, $offset), + ]; + + yield 'shape keys mixing' => [ + static fn(ReadableInterface $source, int $offset): SemanticException + => ShapeKeysMixingException::becauseShapeKeysAreMixed($source, $offset), + ]; + + yield 'variadic with default' => [ + static fn(ReadableInterface $source, int $offset): SemanticException + => VariadicWithDefaultException::becauseVariadicHasDefault($source, $offset), + ]; + } + + /** + * @param \Closure(ReadableInterface,int):SemanticException $factory + */ + #[DataProvider('factoryDataProvider')] + #[Test] + public function everySemanticExceptionIsAParseError(\Closure $factory): void + { + $exception = $factory(self::source(), 0); + + self::assertInstanceOf(ParserExceptionInterface::class, $exception); + self::assertInstanceOf(ParsingException::class, $exception); + } + + /** + * @param \Closure(ReadableInterface,int):SemanticException $factory + */ + #[DataProvider('factoryDataProvider')] + #[Test] + public function everySemanticExceptionKeepsTheSourceAndThePlace(\Closure $factory): void + { + $source = self::source(); + $exception = $factory($source, 14); + + self::assertSame($source, $exception->source); + self::assertSame(14, $exception->token->offset); + self::assertSame(14, $exception->getOffset()); + } + + /** + * @param \Closure(ReadableInterface,int):SemanticException $factory + */ + #[DataProvider('factoryDataProvider')] + #[Test] + public function everySemanticExceptionCarriesTheSourceInItsMessage(\Closure $factory): void + { + $exception = $factory(self::source(), 0); + + self::assertStringEndsWith('in "array{a: int, a: int}"', $exception->getMessage()); + } + + /** + * @param \Closure(ReadableInterface,int):SemanticException $factory + */ + #[DataProvider('factoryDataProvider')] + #[Test] + public function everySemanticExceptionIsPrintedWithItsPlace(\Closure $factory): void + { + $exception = $factory(self::source(), 14); + + self::assertStringContainsString('on line 1 at column 15', (string) $exception); + } + + #[Test] + public function theFeatureNameIsCapitalizedInTheMessage(): void + { + $exception = FeatureNotAllowedException::becauseFeatureIsNotAllowed('shape fields', self::source()); + + self::assertStringStartsWith('Shape fields not allowed', $exception->getMessage()); + } + + #[Test] + public function theUnexpectedSubNodeIsReported(): void + { + $exception = InternalSemanticException::becauseSubNodeIsUnexpected('Example', self::source()); + + self::assertStringStartsWith( + 'Internal error, unexpected square bracket sub-node Example', + $exception->getMessage(), + ); + } + + #[Test] + public function theInvalidConditionalOperatorIsReported(): void + { + $exception = InvalidConditionalOperatorException::becauseConditionalOperatorIsInvalid('~', self::source()); + + self::assertStringStartsWith('Invalid conditional operator "~"', $exception->getMessage()); + } + + #[Test] + public function theDuplicatedShapeFieldIsReported(): void + { + $exception = ShapeFieldDuplicationException::becauseShapeFieldIsDuplicated('key', self::source()); + + self::assertStringStartsWith('Duplicate key "key"', $exception->getMessage()); + } + + #[Test] + public function theMixedShapeKeysAreReported(): void + { + $exception = ShapeKeysMixingException::becauseShapeKeysAreMixed(self::source()); + + self::assertStringStartsWith('Cannot mix explicit and implicit shape keys', $exception->getMessage()); + } + + #[Test] + public function theVariadicWithADefaultIsReported(): void + { + $exception = VariadicWithDefaultException::becauseVariadicHasDefault(self::source()); + + self::assertStringStartsWith('Cannot have variadic param with a default', $exception->getMessage()); + } + + /** + * @param \Closure(ReadableInterface,int):SemanticException $factory + */ + #[DataProvider('factoryDataProvider')] + #[Test] + public function everySemanticExceptionIsToldApartByItsClassAlone(\Closure $factory): void + { + self::assertSame(0, $factory(self::source(), 0)->getCode()); + } +} diff --git a/libs/parser/tests/Internal/StringParserTest.php b/libs/parser/tests/Internal/StringParserTest.php new file mode 100644 index 0000000..d3af246 --- /dev/null +++ b/libs/parser/tests/Internal/StringParserTest.php @@ -0,0 +1,124 @@ + + */ + public static function provideBodies(): iterable + { + yield 'empty' => ['', '']; + yield 'without sequences' => ['example', 'example']; + yield 'line feed' => ['a\nb', "a\nb"]; + yield 'carriage return' => ['a\rb', "a\rb"]; + yield 'tab' => ['a\tb', "a\tb"]; + yield 'vertical tab' => ['a\vb', "a\vb"]; + yield 'escape' => ['a\eb', "a\eb"]; + yield 'form feed' => ['a\fb', "a\fb"]; + yield 'dollar sign' => ['a\$b', 'a$b']; + yield 'double quote' => ['a"b', 'a"b']; + yield 'double quote behind an escaped backslash' => ['a\\\\"b', 'a\\"b']; + yield 'backslash' => ['a\\\\b', 'a\\b']; + yield 'backslash before escape sequence' => ['a\\\\nb', 'a\nb']; + yield 'two backslashes' => ['a\\\\\\\\b', 'a\\\\b']; + yield 'hexadecimal' => ['\x41', 'A']; + yield 'uppercase hexadecimal prefix' => ['\X41', 'A']; + yield 'short hexadecimal' => ['\x9', "\x09"]; + yield 'null byte' => ['\x00', "\0"]; + yield 'unicode' => ['\u{48}', 'H']; + yield 'multibyte unicode' => ['\u{1F600}', "\u{1F600}"]; + yield 'unicode null byte' => ['a\u{0}b', "a\0b"]; + yield 'binary sequence before unicode' => ['\xFF\u{42}', "\xFF" . 'B']; + yield 'backslash before hexadecimal' => ['a\\\\x41b', 'a\x41b']; + yield 'octal' => ['\101', 'A']; + yield 'octal null byte' => ['\0', "\0"]; + yield 'octal overflow is truncated' => ['\777', "\xFF"]; + yield 'backslash before octal' => ['a\\\\101b', 'a\101b']; + yield 'code point above the unicode range' => ['\u{110000}', "\u{FFFD}"]; + yield 'huge code point' => ['\u{FFFFFFF}', "\u{FFFD}"]; + yield 'last unicode code point' => ['\u{10FFFF}', "\u{10FFFF}"]; + yield 'unknown sequence' => ['a\qb', 'a\qb']; + yield 'incomplete unicode sequence' => ['a\u{}b', 'a\u{}b']; + yield 'trailing backslash' => ['a\\', 'a\\']; + yield 'repeated sequences' => ['\x41\x41\u{42}\u{42}', 'AABB']; + yield 'several kinds at once' => ['\x41\u{42}\n\\\\', "AB\n" . '\\']; + } + + #[Test] + #[DataProvider('provideBodies')] + public function bodyIsDecoded(string $body, string $expected): void + { + self::assertSame($expected, StringDecoder::decode($body, true)); + } + + #[Test] + public function rawNullByteIsNotTouched(): void + { + self::assertSame("a\0b", StringDecoder::decode("a\0b", true)); + self::assertSame("a\0\\b", StringDecoder::decode("a\0" . '\\\\b', true)); + } + + #[Test] + public function invalidUtf8IsNotTouched(): void + { + self::assertSame("\xFF\xFE", StringDecoder::decode("\xFF\xFE", true)); + } + + #[Test] + public function decodingIsIdempotentForStringsWithoutBackslashes(): void + { + $decoded = StringDecoder::decode('a\nb', true); + + self::assertSame($decoded, StringDecoder::decode($decoded, true)); + } + + /** + * @return iterable + */ + public static function provideDoubleQuotedStrings(): iterable + { + yield 'empty' => ['""', '']; + yield 'without sequences' => ['"example"', 'example']; + yield 'quote' => ['"a\"b"', 'a"b']; + yield 'sequence is left alone' => ['"a\nb"', 'a\nb']; + yield 'backslash is left alone' => ['"a\\\\b"', 'a\\\\b']; + yield 'quote behind an escaped backslash' => ['"a\\\\\"b"', 'a\\\\"b']; + yield 'quote beside a sequence' => ['"asd\"\nasd"', 'asd"\nasd']; + } + + #[Test] + #[DataProvider('provideDoubleQuotedStrings')] + public function doubleQuotedStringIsUnpacked(string $value, string $expected): void + { + self::assertSame($expected, StringDecoder::unpack($value, true)); + } + + /** + * @return iterable + */ + public static function provideSingleQuotedStrings(): iterable + { + yield 'empty' => ["''", '']; + yield 'without sequences' => ["'example'", 'example']; + yield 'quote' => ["'a\\'b'", "a'b"]; + yield 'backslash is left alone' => ["'a\\\\b'", 'a\\\\b']; + yield 'sequence is left alone' => ["'a\\nb'", 'a\nb']; + yield 'quote behind an escaped backslash' => ["'a\\\\\\'b'", "a\\\\'b"]; + } + + #[Test] + #[DataProvider('provideSingleQuotedStrings')] + public function singleQuotedStringIsUnpacked(string $value, string $expected): void + { + self::assertSame($expected, StringDecoder::unpack($value, false)); + } +} diff --git a/libs/parser/tests/Internal/UtfCharRendererTest.php b/libs/parser/tests/Internal/UtfCharRendererTest.php new file mode 100644 index 0000000..6ee2411 --- /dev/null +++ b/libs/parser/tests/Internal/UtfCharRendererTest.php @@ -0,0 +1,83 @@ +, string}> + */ + public static function provideCodePoints(): iterable + { + yield 'null byte' => [0x00, "\0"]; + yield 'ascii' => [0x41, 'A']; + yield 'last one byte char' => [0x7F, "\x7F"]; + yield 'first two bytes char' => [0x80, "\u{80}"]; + yield 'last two bytes char' => [0x7FF, "\u{7FF}"]; + yield 'first three bytes char' => [0x800, "\u{800}"]; + yield 'last three bytes char' => [0xFFFF, "\u{FFFF}"]; + yield 'first four bytes char' => [0x10000, "\u{10000}"]; + yield 'emoji' => [0x1F600, "\u{1F600}"]; + yield 'last unicode code point' => [0x10FFFF, "\u{10FFFF}"]; + } + + #[Test] + #[DataProvider('provideCodePoints')] + public function codePointIsEncoded(int $code, string $expected): void + { + self::assertSame($expected, UtfCharRenderer::render($code)); + } + + /** + * @return iterable, int<1, 4>}> + */ + public static function provideCodePointLengths(): iterable + { + yield 'one byte' => [0x7F, 1]; + yield 'two bytes' => [0x7FF, 2]; + yield 'three bytes' => [0xFFFF, 3]; + yield 'four bytes' => [0x10FFFF, 4]; + } + + #[Test] + #[DataProvider('provideCodePointLengths')] + public function encodedCharHasExpectedLength(int $code, int $length): void + { + self::assertSame($length, \strlen(UtfCharRenderer::render($code))); + } + + /** + * @return iterable}> + */ + public static function provideOutOfRangeCodePoints(): iterable + { + yield 'first code point above the range' => [0x110000]; + yield 'huge code point' => [0xFFFFFFF]; + yield 'php int max' => [\PHP_INT_MAX]; + } + + #[Test] + #[DataProvider('provideOutOfRangeCodePoints')] + public function outOfRangeCodePointIsEncodedAsReplacementChar(int $code): void + { + self::assertSame("\u{FFFD}", UtfCharRenderer::render($code)); + } + + #[Test] + public function everyEncodedCharIsAValidUtf8Sequence(): void + { + foreach ([0x00, 0x41, 0x7F, 0x80, 0x7FF, 0x800, 0xFFFF, 0x10000, 0x10FFFF] as $code) { + self::assertTrue( + \mb_check_encoding(UtfCharRenderer::render($code), 'UTF-8'), + \sprintf('A code point 0x%X must be encoded as a valid utf-8 sequence', $code), + ); + } + } +} diff --git a/libs/parser/tests/Literal/IntLiteralTest.php b/libs/parser/tests/Literal/IntLiteralTest.php new file mode 100644 index 0000000..43a01c4 --- /dev/null +++ b/libs/parser/tests/Literal/IntLiteralTest.php @@ -0,0 +1,123 @@ +parse($code); + + self::assertInstanceOf(IntLiteralNode::class, $node); + + return $node; + } + + /** + * @return iterable + */ + public static function provideIntegers(): iterable + { + yield 'zero' => ['0', 0, '0']; + yield 'decimal' => ['42', 42, '42']; + yield 'negative decimal' => ['-42', -42, '-42']; + yield 'signed decimal' => ['+42', 42, '42']; + yield 'signed hexadecimal' => ['+0x1F', 31, '31']; + yield 'hexadecimal' => ['0x1F', 31, '31']; + yield 'uppercase hexadecimal prefix' => ['0X1F', 31, '31']; + yield 'negative hexadecimal' => ['-0x10', -16, '-16']; + yield 'binary' => ['0b1010', 10, '10']; + yield 'uppercase binary prefix' => ['0B1010', 10, '10']; + yield 'octal' => ['0o17', 15, '15']; + yield 'uppercase octal prefix' => ['0O17', 15, '15']; + yield 'legacy octal' => ['017', 15, '15']; + yield 'underscored' => ['1_000_000', 1000000, '1000000']; + yield 'underscored hexadecimal' => ['0xFF_FF', 65535, '65535']; + } + + /** + * @param non-empty-string $literal + * @param non-empty-string $decimal + * @throws \Throwable + */ + #[Test] + #[DataProvider('provideIntegers')] + public function integerIsParsedToItsDecimalValue(string $literal, int $value, string $decimal): void + { + $node = $this->literal($literal); + + self::assertSame($value, $node->value); + self::assertSame($decimal, $node->decimal); + } + + /** + * @param non-empty-string $literal + * @param non-empty-string $decimal + * @throws \Throwable + */ + #[Test] + #[DataProvider('provideIntegers')] + public function integerParsingKeepsTheOriginalRepresentation(string $literal, int $value, string $decimal): void + { + self::assertSame($literal, $this->literal($literal)->raw); + } + + /** + * @throws \Throwable + */ + #[Test] + public function integerParsingSupportsPhpIntMin(): void + { + self::assertSame(\PHP_INT_MIN, $this->literal((string) \PHP_INT_MIN)->value); + } + + /** + * @throws \Throwable + */ + #[Test] + public function integerParsingSupportsPhpIntMax(): void + { + self::assertSame(\PHP_INT_MAX, $this->literal((string) \PHP_INT_MAX)->value); + } + + /** + * @throws \Throwable + */ + #[Test] + public function negativeZeroIsParsedAsZero(): void + { + self::assertSame(0, $this->literal('-0')->value); + } + + /** + * @throws \Throwable + */ + #[Test] + public function theOffsetOfALiteralIsTheOneItIsWrittenAt(): void + { + $node = $this->parse('array{a: 42}'); + + self::assertInstanceOf(\TypeLang\Type\NamedTypeNode::class, $node); + self::assertNotNull($node->fields); + + $field = $node->fields->items[0]; + + self::assertInstanceOf(\TypeLang\Type\Shape\NamedFieldNode::class, $field); + self::assertInstanceOf(IntLiteralNode::class, $field->type); + self::assertSame(9, $field->type->offset); + } +} diff --git a/libs/parser/tests/Literal/StringLiteralTest.php b/libs/parser/tests/Literal/StringLiteralTest.php new file mode 100644 index 0000000..4ae83d3 --- /dev/null +++ b/libs/parser/tests/Literal/StringLiteralTest.php @@ -0,0 +1,123 @@ +parse($code); + + self::assertInstanceOf(StringLiteralNode::class, $node); + + return $node; + } + + /** + * @return iterable + */ + public static function provideDoubleQuotedStrings(): iterable + { + yield 'empty' => ['""', '']; + yield 'plain' => ['"example"', 'example']; + yield 'escaped quote' => ['"a\"b"', 'a"b']; + yield 'newline' => ['"a\nb"', "a\nb"]; + yield 'carriage return' => ['"a\rb"', "a\rb"]; + yield 'tab' => ['"a\tb"', "a\tb"]; + yield 'vertical tab' => ['"a\vb"', "a\vb"]; + yield 'escape' => ['"a\eb"', "a\eb"]; + yield 'form feed' => ['"a\fb"', "a\fb"]; + yield 'dollar sign' => ['"a\$b"', 'a$b']; + yield 'escaped backslash' => ['"a\\\\b"', 'a\\b']; + yield 'hexadecimal sequence' => ['"\x41"', 'A']; + yield 'uppercase hexadecimal sequence' => ['"\xFF"', "\xFF"]; + yield 'short hexadecimal sequence' => ['"\x9"', "\x09"]; + yield 'unicode sequence' => ['"\u{48}"', 'H']; + yield 'multibyte unicode sequence' => ['"\u{1F600}"', "\u{1F600}"]; + yield 'unknown escape is kept as is' => ['"a\qb"', 'a\qb']; + yield 'several sequences' => ['"\x41\u{42}\n"', "AB\n"]; + yield 'null byte hexadecimal sequence' => ['"\x00"', "\0"]; + yield 'null byte unicode sequence' => ['"a\u{0}b"', "a\0b"]; + yield 'binary sequence does not break the next one' => ['"\xFF\u{42}"', "\xFF" . 'B']; + yield 'escaped backslash before escaped quote' => ['"a\\\\\"b"', 'a\"b']; + yield 'escaped backslash before escape sequence' => ['"a\\\\nb"', 'a\nb']; + yield 'octal sequence' => ['"\101"', 'A']; + yield 'octal null byte' => ['"\0"', "\0"]; + yield 'octal overflow is truncated' => ['"\777"', "\xFF"]; + yield 'escaped backslash before octal sequence' => ['"a\\\\101b"', 'a\101b']; + yield 'code point above the unicode range' => ['"\u{110000}"', "\u{FFFD}"]; + } + + /** + * @param non-empty-string $literal + * @throws \Throwable + */ + #[Test] + #[DataProvider('provideDoubleQuotedStrings')] + public function doubleQuotedStringIsDecoded(string $literal, string $value): void + { + $node = $this->literal($literal); + + self::assertSame($value, $node->value); + self::assertSame($literal, $node->raw); + } + + /** + * @return iterable + */ + public static function provideSingleQuotedStrings(): iterable + { + yield 'empty' => ["''", '']; + yield 'plain' => ["'example'", 'example']; + yield 'escaped quote' => ["'a\'b'", "a'b"]; + yield 'escaped backslash' => ["'a\\\\b'", 'a\\b']; + yield 'two escaped backslashes' => ["'a\\\\\\\\b'", 'a\\\\b']; + yield 'escaped quote behind an escaped backslash' => ["'a\\\\\\'b'", "a\\'b"]; + yield 'escape sequences are not decoded' => ["'a\\nb'", 'a\\nb']; + yield 'hexadecimal sequence is not decoded' => ["'a\\x41b'", 'a\\x41b']; + yield 'dollar sign is not decoded' => ["'a\$b'", 'a$b']; + } + + /** + * @param non-empty-string $literal + * @throws \Throwable + */ + #[Test] + #[DataProvider('provideSingleQuotedStrings')] + public function singleQuotedStringIsDecoded(string $literal, string $value): void + { + $node = $this->literal($literal); + + self::assertSame($value, $node->value); + self::assertSame($literal, $node->raw); + } + + /** + * A node built by hand derives the raw representation from its value, and + * reading that representation back gives the very same value. + * + * @throws \Throwable + */ + #[Test] + public function derivedRawValueCanBeParsedBack(): void + { + $node = new StringLiteralNode('a"b'); + + self::assertSame($node->value, $this->literal($node->raw)->value); + } +} diff --git a/libs/parser/tests/Syntax/AttributeTest.php b/libs/parser/tests/Syntax/AttributeTest.php deleted file mode 100644 index acf377a..0000000 --- a/libs/parser/tests/Syntax/AttributeTest.php +++ /dev/null @@ -1,144 +0,0 @@ -parseAndPrint('HashMap<#[name("key")] T>')); - } - - public function testMultipleAttributesInOneGroup(): void - { - self::assertSame(<<<'AST' - NamedTypeNode - Name(HashMap) - Identifier(HashMap) - Template\TemplateArgumentListNode - Template\TemplateArgumentNode - NamedTypeNode - Name(T) - Identifier(T) - Attribute\AttributeGroupListNode - Attribute\AttributeGroupNode - Attribute\AttributeNode - Name(name) - Identifier(name) - Attribute\AttributeNode - Name(out) - Identifier(out) - AST, $this->parseAndPrint('HashMap<#[name("key"), out] T>')); - } - - public function testMultipleAttributeGroups(): void - { - self::assertSame(<<<'AST' - NamedTypeNode - Name(HashMap) - Identifier(HashMap) - Template\TemplateArgumentListNode - Template\TemplateArgumentNode - NamedTypeNode - Name(T) - Identifier(T) - Attribute\AttributeGroupListNode - Attribute\AttributeGroupNode - Attribute\AttributeNode - Name(name) - Identifier(name) - Attribute\AttributeGroupNode - Attribute\AttributeNode - Name(out) - Identifier(out) - AST, $this->parseAndPrint('HashMap<#[name("key")] #[out] T>')); - } - - public function testSingleAttributeOnShapeField(): void - { - self::assertSame(<<<'AST' - NamedTypeNode - Name(App\Domain\User) - Identifier(App) - Identifier(Domain) - Identifier(User) - Shape\FieldsListNode(sealed) - Shape\NamedFieldNode(required) - Identifier(userName) - NamedTypeNode - Name(non-empty-string) - Identifier(non-empty-string) - Attribute\AttributeGroupListNode - Attribute\AttributeGroupNode - Attribute\AttributeNode - Name(name) - Identifier(name) - AST, $this->parseAndPrint('App\\Domain\\User{#[name("user_name")] userName: non-empty-string}')); - } - - public function testMultipleAttributeGroupsOnShapeField(): void - { - self::assertSame(<<<'AST' - NamedTypeNode - Name(array) - Identifier(array) - Shape\FieldsListNode(sealed) - Shape\NamedFieldNode(optional) - Identifier(test) - NamedTypeNode - Name(App\Domain\User) - Identifier(App) - Identifier(Domain) - Identifier(User) - Attribute\AttributeGroupListNode - Attribute\AttributeGroupNode - Attribute\AttributeNode - Name(serialize) - Identifier(serialize) - Attribute\AttributeGroupNode - Attribute\AttributeNode - Name(deserialize) - Identifier(deserialize) - AST, $this->parseAndPrint('array{#[serialize("onSerialize")] #[deserialize("onDeserialize")] test?: App\\Domain\\User}')); - } - - public function testTemplateArgumentAttributeAllowsOnlyIdentifiers(): void - { - $this->expectParsingException('unexpected "42"'); - - $this->parse('Collection<#[42] User>'); - } - - public function testShapeFieldAttributeAllowsOnlyIdentifiers(): void - { - $this->expectParsingException('unexpected "42"'); - - $this->parse('Collection{#[42] test?: User}'); - } -} diff --git a/libs/parser/tests/Syntax/CallableTest.php b/libs/parser/tests/Syntax/CallableTest.php index decb6d9..318c8d5 100644 --- a/libs/parser/tests/Syntax/CallableTest.php +++ b/libs/parser/tests/Syntax/CallableTest.php @@ -17,7 +17,6 @@ public function testCallableWithoutParametersAndReturnType(): void self::assertSame(<<<'AST' CallableTypeNode Name(foo) - Identifier(foo) Callable\CallableParameterListNode AST, $this->parseAndPrint('foo()')); } @@ -27,15 +26,12 @@ public function testCallableWithParameterAndReturnType(): void self::assertSame(<<<'AST' CallableTypeNode Name(foo) - Identifier(foo) Callable\CallableParameterListNode - Callable\CallableParameterNode(simple) + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) NamedTypeNode Name(T) - Identifier(T) NamedTypeNode Name(void) - Identifier(void) AST, $this->parseAndPrint('foo(T): void')); } @@ -44,35 +40,28 @@ public function testComplexNestedCallable(): void self::assertSame(<<<'AST' CallableTypeNode Name(a) - Identifier(a) Callable\CallableParameterListNode - Callable\CallableParameterNode(simple) + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) NamedTypeNode Name(int) - Identifier(int) Template\TemplateArgumentListNode Template\TemplateArgumentNode Literal\IntLiteralNode(0) Template\TemplateArgumentNode NamedTypeNode Name(max) - Identifier(max) - Callable\CallableParameterNode(simple) + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) CallableTypeNode Name(c) - Identifier(c) Callable\CallableParameterListNode - Callable\CallableParameterNode(simple) + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) NullableTypeNode NamedTypeNode Name(C) - Identifier(C) NamedTypeNode Name(mixed) - Identifier(mixed) NamedTypeNode Name(void) - Identifier(void) AST, $this->parseAndPrint('a(int<0, max>, c(?C): mixed): void')); } @@ -81,13 +70,12 @@ public function testNamedParameter(): void self::assertSame(<<<'AST' CallableTypeNode Name(foo) - Identifier(foo) Callable\CallableParameterListNode - Callable\CallableParameterNode(simple) + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) NamedTypeNode Name(T) - Identifier(T) - Literal\VariableLiteralNode($name) + VariableNode + Identifier(name) AST, $this->parseAndPrint('foo(T $name)')); } @@ -96,21 +84,18 @@ public function testMixedNamedAndAnonymousParameters(): void self::assertSame(<<<'AST' CallableTypeNode Name(foo) - Identifier(foo) Callable\CallableParameterListNode - Callable\CallableParameterNode(simple) + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) NamedTypeNode Name(A) - Identifier(A) - Literal\VariableLiteralNode($a) - Callable\CallableParameterNode(simple) + VariableNode + Identifier(a) + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) NamedTypeNode Name(B) - Identifier(B) - Callable\CallableParameterNode(simple) + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) NamedTypeNode Name(C) - Identifier(C) AST, $this->parseAndPrint('foo(A $a, B, C)')); } @@ -119,12 +104,10 @@ public function testOutputParameter(): void self::assertSame(<<<'AST' CallableTypeNode Name(foo) - Identifier(foo) Callable\CallableParameterListNode - Callable\CallableParameterNode(output) + Callable\CallableParameterNode(isOutput=true, isVariadic=false, isOptional=false) NamedTypeNode Name(T) - Identifier(T) AST, $this->parseAndPrint('foo(T&)')); } @@ -133,13 +116,12 @@ public function testOutputNamedParameter(): void self::assertSame(<<<'AST' CallableTypeNode Name(foo) - Identifier(foo) Callable\CallableParameterListNode - Callable\CallableParameterNode(output) + Callable\CallableParameterNode(isOutput=true, isVariadic=false, isOptional=false) NamedTypeNode Name(T) - Identifier(T) - Literal\VariableLiteralNode($name) + VariableNode + Identifier(name) AST, $this->parseAndPrint('foo(T &$name)')); } @@ -148,77 +130,51 @@ public function testOptionalParameter(): void self::assertSame(<<<'AST' CallableTypeNode Name(foo) - Identifier(foo) Callable\CallableParameterListNode - Callable\CallableParameterNode(optional) + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=true) NamedTypeNode Name(T) - Identifier(T) AST, $this->parseAndPrint('foo(T=)')); } - public function testVariadicParameterPrefixSyntax(): void + public function testVariadicMarkerCannotPrecedeTheParameterType(): void { - self::assertSame(<<<'AST' - CallableTypeNode - Name(foo) - Identifier(foo) - Callable\CallableParameterListNode - Callable\CallableParameterNode(variadic) - NamedTypeNode - Name(T) - Identifier(T) - AST, $this->parseAndPrint('foo(...T)')); - } + $this->expectParsingException('a parameter list must be closed with a bracket ")"'); - public function testVariadicParameterPostfixSyntax(): void - { - self::assertSame(<<<'AST' - CallableTypeNode - Name(foo) - Identifier(foo) - Callable\CallableParameterListNode - Callable\CallableParameterNode(variadic) - NamedTypeNode - Name(T) - Identifier(T) - AST, $this->parseAndPrint('foo(T...)')); + $this->parse('foo(...T)'); } - public function testVariadicNamedOutputParameter(): void + public function testVariadicParameter(): void { self::assertSame(<<<'AST' CallableTypeNode Name(foo) - Identifier(foo) Callable\CallableParameterListNode - Callable\CallableParameterNode(output, variadic) + Callable\CallableParameterNode(isOutput=false, isVariadic=true, isOptional=false) NamedTypeNode Name(T) - Identifier(T) - Literal\VariableLiteralNode($name) - AST, $this->parseAndPrint('foo(...T &$name)')); + AST, $this->parseAndPrint('foo(T...)')); } - public function testParameterWithoutTypeIsNotAllowed(): void + public function testNameCannotFollowTheDefaultMarker(): void { - $this->expectParsingException('unexpected "$name"'); + $this->expectParsingException('a parameter list must be closed with a bracket ")"'); $this->parse('foo(T= $name)'); } public function testAmpersandMustFollowParameterType(): void { - $this->expectParsingException('unexpected "T"'); + $this->expectParsingException('a parameter list must be closed with a bracket ")"'); $this->parse('foo(&T)'); } - public function testVariadicCannotBeBothPrefixAndPostfix(): void + public function testParameterWithoutATypeIsNotAllowed(): void { - $this->expectParsingException('Either prefix or postfix variadic syntax should be used, but not both'); + $this->expectParsingException('a parameter list must be closed with a bracket ")"'); - $this->parse('foo(...T...)'); + $this->parse('foo($name)'); } public function testVariadicParameterCannotHaveDefault(): void @@ -230,8 +186,52 @@ public function testVariadicParameterCannotHaveDefault(): void public function testLeadingCommaIsNotAllowed(): void { - $this->expectParsingException('unexpected ","'); + $this->expectParsingException('a parameter list must be closed with a bracket ")"'); $this->parse('foo(,T)'); } + + /** + * A reference marker precedes the variadic one, the way PHP itself writes + * it: {@code &...$name} rather than {@code ...&$name}. + */ + public function testReferenceAndVariadicMarkersOfATypedParameterAreOrdered(): void + { + self::assertSame(<<<'AST' + CallableTypeNode + Name(foo) + Callable\CallableParameterListNode + Callable\CallableParameterNode(isOutput=true, isVariadic=true, isOptional=false) + NamedTypeNode + Name(T) + VariableNode + Identifier(name) + AST, $this->parseAndPrint('foo(T &...$name)')); + } + + public function testTypedParameterCannotPutTheReferenceAfterTheVariadic(): void + { + $this->expectParsingException('a parameter list must be closed with a bracket ")"'); + + $this->parse('foo(T ...&$name)'); + } + + public function testModifiersWithoutATypeAreNotAllowed(): void + { + $this->expectParsingException('a parameter list must be closed with a bracket ")"'); + + $this->parse('foo(&...$name)'); + } + + public function testTypedParameterAllowsBothMarkersWithoutAName(): void + { + self::assertSame(<<<'AST' + CallableTypeNode + Name(foo) + Callable\CallableParameterListNode + Callable\CallableParameterNode(isOutput=true, isVariadic=true, isOptional=false) + NamedTypeNode + Name(T) + AST, $this->parseAndPrint('foo(T &...)')); + } } diff --git a/libs/parser/tests/Syntax/ConditionalTest.php b/libs/parser/tests/Syntax/ConditionalTest.php index aacaccf..d219961 100644 --- a/libs/parser/tests/Syntax/ConditionalTest.php +++ b/libs/parser/tests/Syntax/ConditionalTest.php @@ -20,10 +20,6 @@ public static function conditionDataProvider(): iterable { yield 'is' => ['A is B ? C : D', 'EqualConditionNode']; yield 'is not' => ['A is not B ? C : D', 'NotEqualConditionNode']; - yield 'less than' => ['A < B ? C : D', 'LessThanConditionNode']; - yield 'greater than' => ['A > B ? C : D', 'GreaterThanConditionNode']; - yield 'less or equal' => ['A <= B ? C : D', 'LessThanOrEqualConditionNode']; - yield 'greater or equal' => ['A >= B ? C : D', 'GreaterThanOrEqualConditionNode']; } #[DataProvider('conditionDataProvider')] @@ -34,16 +30,12 @@ public function testConditionalOperators(string $type, string $condition): void Condition\\{$condition} NamedTypeNode Name(A) - Identifier(A) NamedTypeNode Name(B) - Identifier(B) NamedTypeNode Name(C) - Identifier(C) NamedTypeNode Name(D) - Identifier(D) AST, $this->parseAndPrint($type)); } @@ -52,19 +44,43 @@ public function testConditionalWithVariableSubject(): void self::assertSame(<<<'AST' TernaryExpressionNode Condition\EqualConditionNode - Literal\VariableLiteralNode($T) + VariableNode + Identifier(T) NamedTypeNode Name(B) - Identifier(B) NamedTypeNode Name(C) - Identifier(C) NamedTypeNode Name(D) - Identifier(D) AST, $this->parseAndPrint('$T is B ? C : D')); } + /** + * A variable stands on either side of the operator, and on both at once. + * + * @return iterable + */ + public static function variableOperandDataProvider(): iterable + { + yield 'on the left' => ['($T is B ? C : D)']; + yield 'on the right' => ['(A is $T ? C : D)']; + yield 'on both sides' => ['($A is $B ? C : D)']; + yield 'this on the left' => ['($this is B ? C : D)']; + yield 'this on the right' => ['(A is $this ? C : D)']; + } + + /** + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('variableOperandDataProvider')] + public function testVariableStandsAsAnOperand(string $type): void + { + $printer = new \TypeLang\Printer\PrettyTypePrinter(); + + self::assertSame($type, $printer->print($this->parse($type))); + } + public function testEqualityOperatorIsNotAllowed(): void { $this->expectParsingException('unexpected "="'); @@ -74,7 +90,7 @@ public function testEqualityOperatorIsNotAllowed(): void public function testInequalityOperatorIsNotAllowed(): void { - $this->expectParsingException('unexpected "!"'); + $this->expectParsingException('unexpected "!="'); $this->parse('A != B ? C : D'); } diff --git a/libs/parser/tests/Syntax/ConstantTest.php b/libs/parser/tests/Syntax/ConstantTest.php index a4bf1e0..749a7d5 100644 --- a/libs/parser/tests/Syntax/ConstantTest.php +++ b/libs/parser/tests/Syntax/ConstantTest.php @@ -4,7 +4,9 @@ namespace TypeLang\Parser\Tests\Syntax; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use TypeLang\Type\ConstMaskNode; /** * Tests for the constant grammar: global constants, class constants and masks. @@ -17,7 +19,6 @@ public function testGlobalConstantIsInterpretedAsNamedType(): void self::assertSame(<<<'AST' NamedTypeNode Name(JSON_THROW_ON_ERROR) - Identifier(JSON_THROW_ON_ERROR) AST, $this->parseAndPrint('JSON_THROW_ON_ERROR')); } @@ -26,8 +27,6 @@ public function testNamespacedConstantIsInterpretedAsNamedType(): void self::assertSame(<<<'AST' NamedTypeNode Name(pcov\version) - Identifier(pcov) - Identifier(version) AST, $this->parseAndPrint('pcov\\version')); } @@ -36,7 +35,6 @@ public function testClassConstant(): void self::assertSame(<<<'AST' ClassConstNode Name(ClassName) - Identifier(ClassName) Identifier(CONSTANT_NAME) AST, $this->parseAndPrint('ClassName::CONSTANT_NAME')); } @@ -46,9 +44,6 @@ public function testNamespacedClassConstant(): void self::assertSame(<<<'AST' ClassConstNode Name(Path\To\ClassName) - Identifier(Path) - Identifier(To) - Identifier(ClassName) Identifier(ANOTHER_CONSTANT_NAME) AST, $this->parseAndPrint('Path\\To\\ClassName::ANOTHER_CONSTANT_NAME')); } @@ -56,21 +51,52 @@ public function testNamespacedClassConstant(): void public function testGlobalConstantMask(): void { self::assertSame(<<<'AST' - ConstMaskNode(JSON_*) - Name(JSON_) + ConstMaskNode(namespaceOrFullyQualified=false) + MaskNode(JSON_*) Identifier(JSON_) + WildcardNode(*) AST, $this->parseAndPrint('JSON_*')); } + public function testGlobalConstantMaskInANamespace(): void + { + self::assertSame(<<<'AST' + ConstMaskNode + MaskNode(JSON_*) + Identifier(JSON_) + WildcardNode(*) + Name(Path\To) + AST, $this->parseAndPrint('Path\\To\\JSON_*')); + } + + public function testGlobalConstantMaskMayBeginWithAnAsterisk(): void + { + self::assertSame(<<<'AST' + ConstMaskNode(namespaceOrFullyQualified=false) + MaskNode(*_SUFFIX) + WildcardNode(*) + Identifier(_SUFFIX) + AST, $this->parseAndPrint('*_SUFFIX')); + } + + public function testGlobalConstantMaskOfAWholeNamespace(): void + { + self::assertSame(<<<'AST' + ConstMaskNode + MaskNode(*) + WildcardNode(*) + Name(Path\To) + AST, $this->parseAndPrint('Path\\To\\*')); + } + public function testClassConstantMask(): void { self::assertSame(<<<'AST' ClassConstMaskNode Name(Path\To\ClassName) - Identifier(Path) - Identifier(To) - Identifier(ClassName) - Identifier(PREFIX_) + MaskNode(PREFIX_*) + Identifier(PREFIX_) + WildcardNode(*) AST, $this->parseAndPrint('Path\\To\\ClassName::PREFIX_*')); } @@ -79,9 +105,8 @@ public function testClassConstantMaskWithoutPrefix(): void self::assertSame(<<<'AST' ClassConstMaskNode Name(Path\To\ClassName) - Identifier(Path) - Identifier(To) - Identifier(ClassName) + MaskNode(*) + WildcardNode(*) AST, $this->parseAndPrint('Path\\To\\ClassName::*')); } @@ -92,17 +117,109 @@ public function testClassConstantCannotContainNamespace(): void $this->parse('ClassName::SOME\\ANY'); } - public function testGlobalConstantMaskCannotOmitPrefix(): void + /** + * A mask standing for every constant there is says nothing worth saying, + * so at least one segment of a name is required. + */ + public function testGlobalConstantMaskCannotBeAnAsteriskAlone(): void { - $this->expectParsingException('unexpected "*"'); + $this->expectParsingException('unexpected end of input'); $this->parse('*'); } - public function testAsteriskMustBeTheFinalCharacter(): void + public function testMaskIsMadeOfEverySegmentItIsWrittenOf(): void + { + self::assertSame(<<<'AST' + ClassConstMaskNode + Name(Path\To\ClassName) + MaskNode(PREFIX_*_SUFFIX) + Identifier(PREFIX_) + WildcardNode(*) + Identifier(_SUFFIX) + AST, $this->parseAndPrint('Path\\To\\ClassName::PREFIX_*_SUFFIX')); + } + + public function testMaskMayBeginWithAnAsterisk(): void { - $this->expectParsingException('unexpected "_SUFFIX"'); + self::assertSame(<<<'AST' + ClassConstMaskNode + Name(Path\To\ClassName) + MaskNode(*_SUFFIX) + WildcardNode(*) + Identifier(_SUFFIX) + AST, $this->parseAndPrint('Path\\To\\ClassName::*_SUFFIX')); + } + + /** + * A mask written with no namespace carries the leading separator itself, + * since there is no name for it to belong to. + */ + public function testFullyQualifiedGlobalConstantMask(): void + { + self::assertSame(<<<'AST' + ConstMaskNode(namespaceOrFullyQualified=true) + MaskNode(JSON_*) + Identifier(JSON_) + WildcardNode(*) + AST, $this->parseAndPrint('\JSON_*')); + } + + /** + * The namespace of a mask keeps the separator it is written with, so the + * name is the one that says the reference is a fully qualified one. + */ + public function testFullyQualifiedGlobalConstantMaskInANamespace(): void + { + self::assertSame(<<<'AST' + ConstMaskNode + MaskNode(JSON_*) + Identifier(JSON_) + WildcardNode(*) + Name(\Path\To) + AST, $this->parseAndPrint('\Path\To\JSON_*')); + } + + public function testFullyQualifiedGlobalConstantMaskOfAWholeNamespace(): void + { + self::assertSame(<<<'AST' + ConstMaskNode + MaskNode(*) + WildcardNode(*) + Name(\Path\To) + AST, $this->parseAndPrint('\Path\To\*')); + } + + /** + * @return iterable + */ + public static function maskQualificationDataProvider(): iterable + { + yield 'a mask alone' => ['JSON_*', false]; + yield 'a mask alone behind a separator' => ['\JSON_*', true]; + yield 'a mask in a namespace' => ['Path\To\JSON_*', false]; + yield 'a mask in a namespace behind a separator' => ['\Path\To\JSON_*', true]; + yield 'a namespace whole' => ['Path\To\*', false]; + yield 'a namespace whole behind a separator' => ['\Path\To\*', true]; + } + + /** + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('maskQualificationDataProvider')] + public function testAMaskSaysWhetherItIsFullyQualified(string $type, bool $expected): void + { + $node = $this->parse($type); + + self::assertInstanceOf(ConstMaskNode::class, $node); + self::assertSame($expected, $node->isFullyQualified()); + self::assertSame($type, (new \TypeLang\Printer\PrettyTypePrinter())->print($node)); + } + public function testTwoAsterisksInARowAreNotAMask(): void + { + $this->expectParsingException('unexpected "*"'); - $this->parse('Path\\To\\ClassName::PREFIX_*_SUFFIX'); + $this->parse('Path\\To\\ClassName::PREFIX_**'); } } diff --git a/libs/parser/tests/Syntax/FeatureToggleTest.php b/libs/parser/tests/Syntax/FeatureToggleTest.php index 1fa42ae..bee253a 100644 --- a/libs/parser/tests/Syntax/FeatureToggleTest.php +++ b/libs/parser/tests/Syntax/FeatureToggleTest.php @@ -31,7 +31,6 @@ public static function disabledFeatureDataProvider(): iterable yield 'lists' => [['lists' => false], 'int[]', 'Square bracket list types not allowed']; yield 'offsets' => [['offsets' => false], 'T[U]', 'Type offsets not allowed']; yield 'conditions' => [['conditions' => false], 'A is B ? C : D', 'Conditional expressions not allowed']; - yield 'attributes' => [['attributes' => false], 'T<#[a] U>', 'Template argument attributes not allowed']; yield 'hints' => [['hints' => false], 'T', 'Template argument hints not allowed']; } @@ -55,7 +54,7 @@ public function testTrailingTextIsRejectedInStrictMode(): void public function testTrailingTextIsAllowedInTolerantMode(): void { - $result = $this->parseTolerant('int and more text'); + $result = $this->partial('int and more text'); $type = $result->type; self::assertInstanceOf(NamedTypeNode::class, $type); diff --git a/libs/parser/tests/Syntax/FloatLiteralTest.php b/libs/parser/tests/Syntax/FloatLiteralTest.php index 66f2dc9..190e725 100644 --- a/libs/parser/tests/Syntax/FloatLiteralTest.php +++ b/libs/parser/tests/Syntax/FloatLiteralTest.php @@ -23,10 +23,43 @@ public static function validFloatDataProvider(): iterable yield 'non-prefixed' => ['.9', 0.9]; yield 'non-suffixed' => ['1.', 1.0]; yield 'negative' => ['-0.9', -0.9]; + yield 'signed' => ['+0.9', 0.9]; + yield 'signed non-prefixed' => ['+.9', 0.9]; yield 'scientific' => ['10e2', 1000.0]; yield 'scientific uppercase' => ['10E2', 1000.0]; yield 'scientific negative exponent' => ['10e-2', 0.1]; + yield 'scientific signed exponent' => ['10e+2', 1000.0]; + yield 'signed scientific' => ['+10e2', 1000.0]; + yield 'signed scientific with signed exponent' => ['+1.5e+3', 1500.0]; + + yield 'separated integer part' => ['4_4.', 44.0]; + yield 'separated fraction part' => ['.4_2', 0.42]; + yield 'separated exponent' => ['42e2_3', 42e23]; + yield 'separated signed exponent' => ['42e+2_3', 42e23]; + yield 'separated in every part' => ['2_3.4_5e-6_7', 23.45e-67]; + } + + /** + * A separator is only allowed between two digits. + * + * @return iterable + */ + public static function invalidSeparatorDataProvider(): iterable + { + yield 'before the dot' => ['1_.5', 'unexpected "_"']; + yield 'after the dot' => ['1._5', 'unexpected "_5"']; + yield 'before the exponent' => ['1.5_e3', 'unexpected "_e3"']; + yield 'inside the exponent' => ['1.5e_3', 'unexpected "e_3"']; + yield 'trailing' => ['1e3_', 'unexpected "_"']; + } + + #[DataProvider('invalidSeparatorDataProvider')] + public function testSeparatorBelongsBetweenDigits(string $type, string $message): void + { + $this->expectParsingException($message); + + $this->parse($type); } #[DataProvider('validFloatDataProvider')] diff --git a/libs/parser/tests/Syntax/GenericTest.php b/libs/parser/tests/Syntax/GenericTest.php index caf897f..0c570f6 100644 --- a/libs/parser/tests/Syntax/GenericTest.php +++ b/libs/parser/tests/Syntax/GenericTest.php @@ -17,18 +17,13 @@ public function testTemplateArguments(): void self::assertSame(<<<'AST' NamedTypeNode Name(Path\To\ExampleClass) - Identifier(Path) - Identifier(To) - Identifier(ExampleClass) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(T) - Identifier(T) Template\TemplateArgumentNode NamedTypeNode Name(U) - Identifier(U) AST, $this->parseAndPrint('Path\\To\\ExampleClass')); } @@ -37,12 +32,10 @@ public function testSingleTemplateArgument(): void self::assertSame(<<<'AST' NamedTypeNode Name(Collection) - Identifier(Collection) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(User) - Identifier(User) AST, $this->parseAndPrint('Collection')); } @@ -51,28 +44,23 @@ public function testNestedTemplateArguments(): void self::assertSame(<<<'AST' NamedTypeNode Name(iterable) - Identifier(iterable) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(int) - Identifier(int) Template\TemplateArgumentListNode Template\TemplateArgumentNode Literal\IntLiteralNode(0) Template\TemplateArgumentNode NamedTypeNode Name(max) - Identifier(max) Template\TemplateArgumentNode NamedTypeNode Name(Collection) - Identifier(Collection) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(User) - Identifier(User) AST, $this->parseAndPrint('iterable, Collection>')); } @@ -81,16 +69,13 @@ public function testTrailingCommaIsAllowed(): void self::assertSame(<<<'AST' NamedTypeNode Name(HashMap) - Identifier(HashMap) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(Request) - Identifier(Request) Template\TemplateArgumentNode NamedTypeNode Name(User) - Identifier(User) AST, $this->parseAndPrint('HashMap')); } @@ -99,17 +84,14 @@ public function testCallSiteHint(): void self::assertSame(<<<'AST' NamedTypeNode Name(HashMap) - Identifier(HashMap) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(array-key) - Identifier(array-key) Template\TemplateArgumentNode - Identifier(covariant) NamedTypeNode Name(Request) - Identifier(Request) + Identifier(covariant) AST, $this->parseAndPrint('HashMap')); } @@ -123,13 +105,10 @@ public function testHintLikeIdentifierWithoutSpaceIsRelativeName(): void self::assertSame(<<<'AST' NamedTypeNode Name(Type) - Identifier(Type) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(out\Some) - Identifier(out) - Identifier(Some) AST, $this->parseAndPrint('Type')); } @@ -143,13 +122,11 @@ public function testHintFollowedBySpaceAndFullyQualifiedName(): void self::assertSame(<<<'AST' NamedTypeNode Name(Type) - Identifier(Type) Template\TemplateArgumentListNode Template\TemplateArgumentNode - Identifier(out) NamedTypeNode Name(\Some) - Identifier(Some) + Identifier(out) AST, $this->parseAndPrint('Type')); } @@ -162,14 +139,10 @@ public function testHintLikeIdentifierWithoutSpaceIsNestedRelativeName(): void self::assertSame(<<<'AST' NamedTypeNode Name(Type) - Identifier(Type) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(out\Some\Deep) - Identifier(out) - Identifier(Some) - Identifier(Deep) AST, $this->parseAndPrint('Type')); } @@ -182,40 +155,43 @@ public function testHintFollowedBySpaceAndRelativeName(): void self::assertSame(<<<'AST' NamedTypeNode Name(Type) - Identifier(Type) Template\TemplateArgumentListNode Template\TemplateArgumentNode - Identifier(out) NamedTypeNode Name(Some) - Identifier(Some) + Identifier(out) AST, $this->parseAndPrint('Type')); } public function testMissingTemplateArgument(): void { - $this->expectParsingException('unexpected ">"'); + $this->expectParsingException('an argument list must carry at least one argument'); $this->parse('example<>'); } public function testLeadingCommaIsNotAllowed(): void { - $this->expectParsingException('unexpected ","'); + $this->expectParsingException('an argument list must carry at least one argument'); $this->parse('example<,T>'); } public function testHintAllowsOnlyIdentifiers(): void { - $this->expectParsingException('unexpected "User"'); + $this->expectParsingException('an argument list must be closed with a bracket ">"'); $this->parse('Collection<42 User>'); } + /** + * A second hint reads as the bound of a template parameter, and a + * parameter list belongs to a callable, so the statement is refused + * where the parenthesis it would go on with is missing. + */ public function testMultipleHintsAreNotAllowed(): void { - $this->expectParsingException('unexpected "Request"'); + $this->expectParsingException('unexpected end of input'); $this->parse('HashMap'); } diff --git a/libs/parser/tests/Syntax/GroupingTest.php b/libs/parser/tests/Syntax/GroupingTest.php new file mode 100644 index 0000000..6fc7461 --- /dev/null +++ b/libs/parser/tests/Syntax/GroupingTest.php @@ -0,0 +1,322 @@ +parseAndPrint('int&string|float')); + } + + public function testParenthesesTurnAUnionIntoAMemberOfAnIntersection(): void + { + self::assertSame(<<<'AST' + IntersectionTypeNode + UnionTypeNode + NamedTypeNode + Name(A) + NamedTypeNode + Name(B) + NamedTypeNode + Name(C) + AST, $this->parseAndPrint('(A|B)&C')); + } + + /** + * @return iterable + */ + public static function parenthesizedNameDataProvider(): iterable + { + yield 'a single pair' => ['(A)']; + yield 'a pair of pairs' => ['((A))']; + yield 'spaces inside' => ['( A )']; + } + + /** + * Parentheses group and nothing else, so a pair that groups a single type + * reaches no tree. + * + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('parenthesizedNameDataProvider')] + public function testParenthesesAroundASingleTypeAreOfNoMeaning(string $type): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(A) + AST, $this->parseAndPrint($type)); + } + + public function testTheQuestionMarkReachesOverAWholeGroup(): void + { + self::assertSame(<<<'AST' + NullableTypeNode + UnionTypeNode + NamedTypeNode + Name(A) + NamedTypeNode + Name(B) + AST, $this->parseAndPrint('?(A|B)')); + } + + /** + * A "[]" is read before the "?" in front of it, so the list is the one + * made nullable and not its element. + */ + public function testTheQuestionMarkReachesOverAListAndNotOverItsElement(): void + { + self::assertSame(<<<'AST' + NullableTypeNode + TypesListNode + NamedTypeNode + Name(int) + AST, $this->parseAndPrint('?int[]')); + } + + public function testAListIsMadeOfAWholeGroup(): void + { + self::assertSame(<<<'AST' + TypesListNode + UnionTypeNode + NamedTypeNode + Name(A) + NamedTypeNode + Name(B) + AST, $this->parseAndPrint('(A|B)[]')); + } + + public function testListsNestFromTheInsideOut(): void + { + self::assertSame(<<<'AST' + TypesListNode + TypesListNode + NamedTypeNode + Name(int) + AST, $this->parseAndPrint('int[][]')); + } + + public function testAListFollowsAnOffsetItIsWrittenBehind(): void + { + self::assertSame(<<<'AST' + TypesListNode + TypeOffsetAccessNode + NamedTypeNode + Name(K) + NamedTypeNode + Name(T) + AST, $this->parseAndPrint('T[K][]')); + + $node = $this->parse('T[K][]'); + + self::assertInstanceOf(TypesListNode::class, $node); + self::assertInstanceOf(TypeOffsetAccessNode::class, $node->type); + self::assertInstanceOf(NamedTypeNode::class, $node->type->access); + self::assertSame('K', $node->type->access->name->toString()); + } + + /** + * @return iterable + */ + public static function listedPrimaryTypeDataProvider(): iterable + { + yield 'a generic type' => ['list[]']; + yield 'a shape' => ['array{a: int}[]']; + yield 'a class constant' => ['T::CONST[]']; + yield 'a constant mask' => ['JSON_*[]']; + yield 'a literal' => ["'a'[]"]; + yield 'a group' => ['(int)[]']; + } + + /** + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('listedPrimaryTypeDataProvider')] + public function testEveryPrimaryTypeCarriesAList(string $type): void + { + self::assertStringStartsWith('TypesListNode', $this->parseAndPrint($type)); + } + + public function testCallablesNestInTheirParameters(): void + { + self::assertSame(<<<'AST' + CallableTypeNode + Name(callable) + Callable\CallableParameterListNode + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) + CallableTypeNode + Name(callable) + Callable\CallableParameterListNode + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) + NamedTypeNode + Name(int) + NamedTypeNode + Name(string) + NamedTypeNode + Name(void) + AST, $this->parseAndPrint('callable(callable(int): string): void')); + } + + public function testACallableReturnsACallable(): void + { + self::assertSame(<<<'AST' + CallableTypeNode + Name(callable) + Callable\CallableParameterListNode + CallableTypeNode + Name(callable) + Callable\CallableParameterListNode + NamedTypeNode + Name(void) + AST, $this->parseAndPrint('callable(): callable(): void')); + } + + /** + * The branch a condition chooses is a type whole, so a condition of its + * own stands there as well. + */ + public function testAConditionNestsInTheBranchOfAnother(): void + { + self::assertSame(<<<'AST' + TernaryExpressionNode + Condition\EqualConditionNode + NamedTypeNode + Name(A) + NamedTypeNode + Name(B) + TernaryExpressionNode + Condition\EqualConditionNode + NamedTypeNode + Name(C) + NamedTypeNode + Name(D) + NamedTypeNode + Name(E) + NamedTypeNode + Name(F) + NamedTypeNode + Name(G) + AST, $this->parseAndPrint('A is B ? C is D ? E : F : G')); + } + + public function testAUnionReachesIntoTheBranchOfACondition(): void + { + self::assertSame(<<<'AST' + TernaryExpressionNode + Condition\EqualConditionNode + NamedTypeNode + Name(A) + NamedTypeNode + Name(B) + NamedTypeNode + Name(C) + UnionTypeNode + NamedTypeNode + Name(D) + NamedTypeNode + Name(E) + AST, $this->parseAndPrint('A is B ? C : D|E')); + } + + public function testTheSubjectOfAConditionIsAWholeUnion(): void + { + self::assertSame(<<<'AST' + TernaryExpressionNode + Condition\EqualConditionNode + UnionTypeNode + NamedTypeNode + Name(int) + NamedTypeNode + Name(string) + NamedTypeNode + Name(A) + NamedTypeNode + Name(B) + NamedTypeNode + Name(C) + AST, $this->parseAndPrint('int|string is A ? B : C')); + } + + public function testShapesNestInOneAnother(): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(array) + Shape\FieldsListNode(isSealed=true) + Shape\NamedFieldNode(isOptional=false) + Identifier(a) + NamedTypeNode + Name(array) + Shape\FieldsListNode(isSealed=true) + Shape\NamedFieldNode(isOptional=false) + Identifier(b) + NamedTypeNode + Name(int) + AST, $this->parseAndPrint('array{a: array{b: int}}')); + } + + public function testAGroupStandsAsAShapeValue(): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(array) + Shape\FieldsListNode(isSealed=true) + Shape\NamedFieldNode(isOptional=false) + Identifier(a) + UnionTypeNode + NamedTypeNode + Name(A) + NamedTypeNode + Name(B) + AST, $this->parseAndPrint('array{a: (A|B)}')); + } + + public function testAnEmptyGroupIsNoType(): void + { + $this->expectParsingException('unexpected ")"'); + + $this->parse('()'); + } + + public function testAGroupIsClosedByTheParenthesisItIsOpenedWith(): void + { + $this->expectParsingException('a group must be closed with a bracket ")"'); + + $this->parse('(int'); + } + + public function testAnAngleBracketOfItsOwnFollowsNoArgumentList(): void + { + $this->expectParsingException('unexpected ">"'); + + $this->parse('Some>'); + } +} diff --git a/libs/parser/tests/Syntax/IntLiteralTest.php b/libs/parser/tests/Syntax/IntLiteralTest.php index 5e533f3..32a1e54 100644 --- a/libs/parser/tests/Syntax/IntLiteralTest.php +++ b/libs/parser/tests/Syntax/IntLiteralTest.php @@ -22,12 +22,14 @@ public static function validIntDataProvider(): iterable yield 'zero' => ['0', 0]; yield 'decimal' => ['42', 42]; yield 'negative decimal' => ['-42', -42]; + yield 'signed decimal' => ['+42', 42]; yield 'decimal with underscore' => ['1_000_000', 1000000]; yield 'binary' => ['0b10101101', 173]; yield 'binary with underscore' => ['0b10_10_11_01', 173]; yield 'binary uppercase prefix' => ['0B1010', 10]; yield 'negative binary' => ['-0b1010', -10]; + yield 'signed binary' => ['+0b1010', 10]; yield 'octal' => ['0o42', 34]; yield 'octal uppercase prefix' => ['0O42', 34]; @@ -37,6 +39,40 @@ public static function validIntDataProvider(): iterable yield 'hexadecimal' => ['0xDEAD', 57005]; yield 'hexadecimal mixed case' => ['0XDeaD', 57005]; yield 'hexadecimal with underscore' => ['0xDEAD_BEEF', 3735928559]; + yield 'signed hexadecimal' => ['+0xDEAD', 57005]; + yield 'signed octal' => ['+0o42', 34]; + + yield 'legacy octal of the zero alone' => ['00', 0]; + yield 'legacy octal is the prefixed one' => ['0123', 0o123]; + + yield 'separated decimal' => ['42_04', 4204]; + yield 'separated legacy octal' => ['04_23', 275]; + yield 'separated legacy octal beside the zero' => ['0_42', 34]; + yield 'separated octal' => ['0o6_5_5', 429]; + yield 'separated hexadecimal' => ['0xFE_DE', 65246]; + yield 'separated binary' => ['0b0001_1000', 24]; + } + + /** + * A separator is only allowed between two digits. + * + * @return iterable + */ + public static function invalidSeparatorDataProvider(): iterable + { + yield 'trailing' => ['42_', 'unexpected "_"']; + yield 'doubled' => ['4__2', 'unexpected "__2"']; + yield 'after the hexadecimal prefix' => ['0x_FF', 'unexpected "x_FF"']; + yield 'after the binary prefix' => ['0b_1', 'unexpected "b_1"']; + yield 'after the octal prefix' => ['0o_7', 'unexpected "o_7"']; + } + + #[DataProvider('invalidSeparatorDataProvider')] + public function testSeparatorBelongsBetweenDigits(string $type, string $message): void + { + $this->expectParsingException($message); + + $this->parse($type); } #[DataProvider('validIntDataProvider')] @@ -62,6 +98,26 @@ public function testOctalAllowsOnlyDigitsUpToSeven(): void $this->parse('0o4281'); } + /** + * A leading zero means an octal, so a digit outside that radix cannot + * follow one. + * + * @return iterable + */ + public static function leadingZeroDataProvider(): iterable + { + yield 'eight' => ['08', 'unexpected "8"']; + yield 'nine' => ['09_1', 'unexpected "9_1"']; + } + + #[DataProvider('leadingZeroDataProvider')] + public function testDecimalCannotBeginWithZero(string $type, string $message): void + { + $this->expectParsingException($message); + + $this->parse($type); + } + public function testHexadecimalAllowsOnlyHexDigits(): void { $this->expectParsingException(); diff --git a/libs/parser/tests/Syntax/ListTest.php b/libs/parser/tests/Syntax/ListTest.php index 500f953..19741e9 100644 --- a/libs/parser/tests/Syntax/ListTest.php +++ b/libs/parser/tests/Syntax/ListTest.php @@ -18,7 +18,6 @@ public function testSimpleList(): void TypesListNode NamedTypeNode Name(User) - Identifier(User) AST, $this->parseAndPrint('User[]')); } @@ -29,7 +28,6 @@ public function testNestedList(): void TypesListNode NamedTypeNode Name(User) - Identifier(User) AST, $this->parseAndPrint('User[][]')); } } diff --git a/libs/parser/tests/Syntax/LogicalTest.php b/libs/parser/tests/Syntax/LogicalTest.php index bf99348..b67cd54 100644 --- a/libs/parser/tests/Syntax/LogicalTest.php +++ b/libs/parser/tests/Syntax/LogicalTest.php @@ -19,13 +19,10 @@ public function testUnionType(): void UnionTypeNode NamedTypeNode Name(A) - Identifier(A) NamedTypeNode Name(B) - Identifier(B) NamedTypeNode Name(C) - Identifier(C) AST, $this->parseAndPrint('A | B | C')); } @@ -35,13 +32,10 @@ public function testIntersectionType(): void IntersectionTypeNode NamedTypeNode Name(A) - Identifier(A) NamedTypeNode Name(B) - Identifier(B) NamedTypeNode Name(C) - Identifier(C) AST, $this->parseAndPrint('A & B & C')); } @@ -51,7 +45,6 @@ public function testNullableType(): void NullableTypeNode NamedTypeNode Name(Example) - Identifier(Example) AST, $this->parseAndPrint('?Example')); } @@ -62,13 +55,10 @@ public function testDisjunctiveNormalForm(): void IntersectionTypeNode NamedTypeNode Name(A) - Identifier(A) NamedTypeNode Name(B) - Identifier(B) NamedTypeNode Name(C) - Identifier(C) AST, $this->parseAndPrint('(A & B) | C')); } @@ -79,13 +69,10 @@ public function testConjunctiveNormalForm(): void UnionTypeNode NamedTypeNode Name(A) - Identifier(A) NamedTypeNode Name(B) - Identifier(B) NamedTypeNode Name(C) - Identifier(C) AST, $this->parseAndPrint('(A | B) & C')); } @@ -98,7 +85,7 @@ public function testNullableQuestionMarkMustBeBeforeType(): void public function testDanglingUnionDelimiter(): void { - $this->expectParsingException('unexpected end of input'); + $this->expectParsingException('a union type must carry a type after the vertical bar'); $this->parse('int |'); } @@ -112,7 +99,7 @@ public function testLeadingUnionDelimiter(): void public function testDanglingIntersectionDelimiter(): void { - $this->expectParsingException('unexpected end of input'); + $this->expectParsingException('an intersection type must carry a type after the ampersand'); $this->parse('string &'); } diff --git a/libs/parser/tests/Syntax/MalformedInputTest.php b/libs/parser/tests/Syntax/MalformedInputTest.php new file mode 100644 index 0000000..646dbc5 --- /dev/null +++ b/libs/parser/tests/Syntax/MalformedInputTest.php @@ -0,0 +1,271 @@ + + */ + public static function emptyInputDataProvider(): iterable + { + yield 'nothing at all' => ['']; + yield 'spaces' => [' ']; + yield 'a line terminator' => ["\n"]; + yield 'a tabulation' => ["\t"]; + } + + /** + * @throws \Throwable + */ + #[DataProvider('emptyInputDataProvider')] + public function testAnEmptyInputIsNoType(string $type): void + { + $this->expectParsingException('unexpected end of input'); + + $this->parse($type); + } + + /** + * A type that is cut short ends where the input does, so the reading + * stops at the end of it rather than at a token. + * + * @return iterable + */ + public static function truncatedInputDataProvider(): iterable + { + yield 'a condition without its comparand' => ['A is', 'unexpected end of input']; + + // The grammar describes what is missing wherever it can, and the + // message it carries is reported instead of the token that is absent. + yield 'a dangling union' => [ + 'int|', + 'a union type must carry a type after the vertical bar "|"', + ]; + yield 'a dangling intersection' => [ + 'int&', + 'an intersection type must carry a type after the ampersand "&"', + ]; + yield 'a question mark alone' => [ + '?', + 'a nullable type must carry the type it makes nullable', + ]; + yield 'a name that is a separator short' => [ + 'Some\\', + 'a name must carry a segment after the separator', + ]; + yield 'a class constant that is a name short' => [ + 'Some::', + 'a class constant must carry a name after the double colon', + ]; + yield 'an unclosed shape' => ['array{', 'a shape must be closed with a brace "}"']; + yield 'an unclosed field' => [ + 'array{a:', + 'a shape field must carry a type after the colon ":"', + ]; + yield 'an unclosed argument list' => [ + 'Some<', + 'an argument list must carry at least one argument', + ]; + yield 'an unclosed parameter list' => [ + 'callable(', + 'a parameter list must be closed with a bracket ")"', + ]; + yield 'an unclosed offset' => ['int[', 'an offset must be closed with a bracket "]"']; + yield 'an unclosed group' => ['(int', 'a group must be closed with a bracket ")"']; + yield 'a condition without its branches' => [ + 'A is B ?', + 'a condition must carry the type it is true of', + ]; + yield 'a condition of a single branch' => [ + 'A is B ? C', + 'a condition must be parted with a colon ":"', + ]; + } + + /** + * @param non-empty-string $type + * @param non-empty-string $message + * @throws \Throwable + */ + #[DataProvider('truncatedInputDataProvider')] + public function testATruncatedTypeIsRefused(string $type, string $message): void + { + $this->expectParsingException($message); + + $this->parse($type); + } + + /** + * @return iterable + */ + public static function strayTokenDataProvider(): iterable + { + yield 'a union delimiter' => ['|', 'unexpected "|"']; + yield 'an intersection delimiter' => ['&', 'unexpected "&"']; + yield 'a comma' => [',', 'unexpected ","']; + yield 'a colon' => [':', 'unexpected ":"']; + yield 'a double colon' => ['::CONST', 'unexpected "::"']; + yield 'a closing brace' => ['}', 'unexpected "}"']; + yield 'a closing bracket' => [']', 'unexpected "]"']; + yield 'a closing parenthesis' => [')', 'unexpected ")"']; + yield 'an ellipsis' => ['...', 'unexpected "..."']; + yield 'an assignment' => ['=', 'unexpected "="']; + } + + /** + * @param non-empty-string $type + * @param non-empty-string $message + * @throws \Throwable + */ + #[DataProvider('strayTokenDataProvider')] + public function testPunctuationAloneIsNoType(string $type, string $message): void + { + $this->expectParsingException($message); + + $this->parse($type); + } + + /** + * @return iterable + */ + public static function trailingTokenDataProvider(): iterable + { + yield 'a question mark behind a type' => ['int?', 'unexpected "?"']; + yield 'an angle bracket too many' => ['Some>', 'unexpected ">"']; + yield 'a brace too many' => ['array{a: int}}', 'unexpected "}"']; + yield 'a bracket too many' => ['int[]]', 'unexpected "]"']; + yield 'an empty argument list' => ['Some<>', 'an argument list must carry at least one argument']; + yield 'an empty group' => ['()', 'unexpected ")"']; + yield 'a constant of a generic type' => ['Some::CONST', 'unexpected "::"']; + yield 'a second type' => ['int string', 'unexpected "string"']; + } + + /** + * @param non-empty-string $type + * @param non-empty-string $message + * @throws \Throwable + */ + #[DataProvider('trailingTokenDataProvider')] + public function testATokenThatFollowsAWholeTypeIsRefused(string $type, string $message): void + { + $this->expectParsingException($message); + + $this->parse($type); + } + + /** + * A partial reading keeps whatever type it has read and says where it + * stopped, rather than refusing the input whole. + * + * @return iterable}> + */ + public static function tolerantInputDataProvider(): iterable + { + yield 'a name and a tail' => ['int foo bar', 'int', 4]; + yield 'a union and a tail' => ['int|string extra', 'int|string', 11]; + yield 'a shape and a tail' => ['array{a: int} tail', 'array{a: int}', 14]; + yield 'a list and an ellipsis' => ['int[] ...', 'int[]', 6]; + } + + /** + * @param non-empty-string $type + * @param non-empty-string $expected + * @param int<0, max> $offset + * @throws \Throwable + */ + #[DataProvider('tolerantInputDataProvider')] + public function testAPartialReadingStopsAtWhatItCannotRead( + string $type, + string $expected, + int $offset, + ): void { + $result = $this->partial($type); + + self::assertInstanceOf(PartialParsedResult::class, $result); + self::assertSame($expected, (new \TypeLang\Printer\PrettyTypePrinter())->print($result->type)); + self::assertSame($offset, $result->offset); + } + + /** + * A source read in full is no partial one, so it carries no offset of + * its own. + */ + public function testAWholeTypeIsNoPartialReading(): void + { + $result = $this->partial('array{a: int}'); + + self::assertInstanceOf(SuccessfulParsedResult::class, $result); + self::assertNotInstanceOf(PartialParsedResult::class, $result); + } + + /** + * A partial reading is tolerant of a tail alone, so an input that opens + * no type at all is a failure all the same. + */ + public function testAPartialReadingRefusesAnInputThatOpensNoType(): void + { + $result = $this->partial('|int'); + + self::assertInstanceOf(FailureParsedResult::class, $result); + self::assertStringContainsString('unexpected "|"', $result->message); + self::assertSame(0, $result->offset); + self::assertSame(1, $result->position->line); + self::assertSame(1, $result->position->column); + } + + /** + * @return iterable}> + */ + public static function checkedInputDataProvider(): iterable + { + yield 'a whole type' => ['array{a: int}', SuccessfulCheckResult::class]; + yield 'a type and a tail' => ['array{a: int} tail', PartialCheckResult::class]; + yield 'no type at all' => ['|int', FailureCheckResult::class]; + yield 'an empty source' => ['', FailureCheckResult::class]; + } + + /** + * A check builds nothing, so it says what stands in the way and nothing + * else. + * + * @param non-empty-string $type + * @param class-string $expected + * @throws \Throwable + */ + #[DataProvider('checkedInputDataProvider')] + public function testACheckTellsWhetherASourceIsAWholeType(string $type, string $expected): void + { + self::assertInstanceOf($expected, $this->validate($type)); + } + + /** + * A source read in part is a failure of a check, since a check asks + * about the source whole. + */ + public function testAPartialCheckIsAFailure(): void + { + $result = $this->validate('array{a: int} tail'); + + self::assertInstanceOf(FailureCheckResult::class, $result); + self::assertSame(14, $result->offset); + self::assertStringContainsString('unexpected "tail"', $result->message); + } +} diff --git a/libs/parser/tests/Syntax/NameTest.php b/libs/parser/tests/Syntax/NameTest.php index b68646f..93a4fbb 100644 --- a/libs/parser/tests/Syntax/NameTest.php +++ b/libs/parser/tests/Syntax/NameTest.php @@ -39,7 +39,7 @@ public function testValidNames(string $type, string $expected): void self::assertInstanceOf(NamedTypeNode::class, $statement); self::assertSame($expected, $statement->name->toString()); - self::assertTrue($statement->name->isSimple); + self::assertTrue($statement->name->isSimple()); } public function testRelativeNamespacedName(): void @@ -47,8 +47,6 @@ public function testRelativeNamespacedName(): void self::assertSame(<<<'AST' NamedTypeNode Name(Example\Name) - Identifier(Example) - Identifier(Name) AST, $this->parseAndPrint('Example\\Name')); } @@ -57,9 +55,6 @@ public function testAbsoluteNamespacedName(): void self::assertSame(<<<'AST' NamedTypeNode Name(\Absolute\Type\Name) - Identifier(Absolute) - Identifier(Type) - Identifier(Name) AST, $this->parseAndPrint('\\Absolute\\Type\\Name')); } @@ -82,13 +77,69 @@ public function testKeywordAsExplicitTypeReference(string $keyword): void self::assertSame(<<parseAndPrint('\\' . $keyword)); } + /** + * A source text is read as bytes, and every byte of a character outside + * of ASCII is a letter, so a name stands in whatever script it is + * written in. + * + * @return iterable + */ + public static function nonAsciiNameDataProvider(): iterable + { + yield 'latin-1' => ['Über', 'Über']; + yield 'cyrillic' => ['Тип', 'Тип']; + yield 'cjk' => ['你好', '你好']; + yield 'a namespaced name' => ['Проект\Тип', 'Проект\Тип']; + yield 'beside an ascii part' => ['Проект\Type', 'Проект\Type']; + yield 'with a dash' => ['non-empty-Тип', 'non-empty-Тип']; + yield 'with a digit' => ['Тип42', 'Тип42']; + } + + /** + * @param non-empty-string $type + * @param non-empty-string $expected + * @throws \Throwable + */ + #[DataProvider('nonAsciiNameDataProvider')] + public function testANameStandsInAnyScript(string $type, string $expected): void + { + self::assertSame( + $expected, + (new \TypeLang\Printer\PrettyTypePrinter())->print($this->parse($type)), + ); + } + + public function testANonAsciiNameStandsAsAShapeKey(): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(array) + Shape\FieldsListNode(isSealed=true) + Shape\NamedFieldNode(isOptional=false) + Identifier(ключ) + NamedTypeNode + Name(int) + AST, $this->parseAndPrint('array{ключ: int}')); + } + + public function testANonAsciiNameStandsAsATemplateArgument(): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(list) + Template\TemplateArgumentListNode + Template\TemplateArgumentNode + NamedTypeNode + Name(Тип) + AST, $this->parseAndPrint('list<Тип>')); + } public function testNameCannotStartWithDigit(): void { - $this->expectParsingException('unexpected "invalid_name_0"'); + // The underscore belongs to the name, not to the number + $this->expectParsingException('unexpected "_invalid_name_0"'); $this->parse('0_invalid_name_0'); } @@ -102,14 +153,14 @@ public function testNameCannotStartWithDigitFollowedByLetters(): void public function testNameCannotStartWithDash(): void { - $this->expectParsingException('unexpected "-"'); + $this->expectParsingException('unexpected "-foo"'); $this->parse('-foo'); } public function testNamespaceCannotEndWithDelimiter(): void { - $this->expectParsingException('unexpected end of input'); + $this->expectParsingException('a name must carry a segment after the separator'); $this->parse('example\\name\\'); } diff --git a/libs/parser/tests/Syntax/OffsetAccessTest.php b/libs/parser/tests/Syntax/OffsetAccessTest.php index b642321..1c53b66 100644 --- a/libs/parser/tests/Syntax/OffsetAccessTest.php +++ b/libs/parser/tests/Syntax/OffsetAccessTest.php @@ -19,7 +19,6 @@ public function testStringOffset(): void Literal\StringLiteralNode('offset') NamedTypeNode Name(T) - Identifier(T) AST, $this->parseAndPrint("T['offset']")); } @@ -29,10 +28,8 @@ public function testDependentKeyOffset(): void TypeOffsetAccessNode NamedTypeNode Name(U) - Identifier(U) NamedTypeNode Name(T) - Identifier(T) AST, $this->parseAndPrint('T[U]')); } @@ -43,16 +40,13 @@ public function testShapeWithNumericOffset(): void Literal\IntLiteralNode(0) NamedTypeNode Name(array) - Identifier(array) - Shape\FieldsListNode(sealed) - Shape\ImplicitFieldNode(required) + Shape\FieldsListNode(isSealed=true) + Shape\ImplicitFieldNode(isOptional=false) NamedTypeNode Name(int) - Identifier(int) - Shape\ImplicitFieldNode(required) + Shape\ImplicitFieldNode(isOptional=false) NamedTypeNode Name(string) - Identifier(string) AST, $this->parseAndPrint('array{int, string}[0]')); } @@ -62,27 +56,23 @@ public function testComplexOffsetWithGenericsAndShapes(): void TypeOffsetAccessNode NamedTypeNode Name(object) - Identifier(object) - Shape\FieldsListNode(unsealed) - Shape\NamedFieldNode(required) + Shape\FieldsListNode(isSealed=false) + Shape\NamedFieldNode(isOptional=false) Identifier(key) NamedTypeNode Name(int) - Identifier(int) NamedTypeNode Name(T) - Identifier(T) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(U) - Identifier(U) AST, $this->parseAndPrint('T[object{key: int, ...}]')); } public function testOffsetCannotBeDoubleBracketed(): void { - $this->expectParsingException('unexpected "["'); + $this->expectParsingException('an offset must be closed with a bracket "]"'); $this->parse('Collection[[Some]]'); } diff --git a/libs/parser/tests/Syntax/ShapeTest.php b/libs/parser/tests/Syntax/ShapeTest.php index 05d4169..49a7360 100644 --- a/libs/parser/tests/Syntax/ShapeTest.php +++ b/libs/parser/tests/Syntax/ShapeTest.php @@ -4,6 +4,7 @@ namespace TypeLang\Parser\Tests\Syntax; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; /** @@ -17,18 +18,15 @@ public function testNamedExplicitKeys(): void self::assertSame(<<<'AST' NamedTypeNode Name(array) - Identifier(array) - Shape\FieldsListNode(sealed) - Shape\NamedFieldNode(required) + Shape\FieldsListNode(isSealed=true) + Shape\NamedFieldNode(isOptional=false) Identifier(a) NamedTypeNode Name(first) - Identifier(first) - Shape\NamedFieldNode(required) + Shape\NamedFieldNode(isOptional=false) Identifier(b) NamedTypeNode Name(second) - Identifier(second) AST, $this->parseAndPrint('array{a: first, b: second}')); } @@ -37,18 +35,15 @@ public function testNumericExplicitKeys(): void self::assertSame(<<<'AST' NamedTypeNode Name(array) - Identifier(array) - Shape\FieldsListNode(sealed) - Shape\NumericFieldNode(required) + Shape\FieldsListNode(isSealed=true) + Shape\ScalarFieldNode(isOptional=false) Literal\IntLiteralNode(1) NamedTypeNode Name(first) - Identifier(first) - Shape\NumericFieldNode(required) + Shape\ScalarFieldNode(isOptional=false) Literal\IntLiteralNode(42) NamedTypeNode Name(second) - Identifier(second) AST, $this->parseAndPrint('array{1: first, 42: second}')); } @@ -57,18 +52,15 @@ public function testStringExplicitKeys(): void self::assertSame(<<<'AST' NamedTypeNode Name(array) - Identifier(array) - Shape\FieldsListNode(sealed) - Shape\StringNamedFieldNode(required) + Shape\FieldsListNode(isSealed=true) + Shape\ScalarFieldNode(isOptional=false) Literal\StringLiteralNode("name-some") NamedTypeNode Name(first) - Identifier(first) - Shape\StringNamedFieldNode(required) + Shape\ScalarFieldNode(isOptional=false) Literal\StringLiteralNode("escape\nchars") NamedTypeNode Name(second) - Identifier(second) AST, $this->parseAndPrint('array{"name-some": first, "escape\\nchars": second}')); } @@ -77,16 +69,13 @@ public function testImplicitKeys(): void self::assertSame(<<<'AST' NamedTypeNode Name(array) - Identifier(array) - Shape\FieldsListNode(sealed) - Shape\ImplicitFieldNode(required) + Shape\FieldsListNode(isSealed=true) + Shape\ImplicitFieldNode(isOptional=false) NamedTypeNode Name(first) - Identifier(first) - Shape\ImplicitFieldNode(required) + Shape\ImplicitFieldNode(isOptional=false) NamedTypeNode Name(second) - Identifier(second) AST, $this->parseAndPrint('array{first, second}')); } @@ -95,8 +84,7 @@ public function testEmptyShape(): void self::assertSame(<<<'AST' NamedTypeNode Name(array) - Identifier(array) - Shape\FieldsListNode(sealed) + Shape\FieldsListNode(isSealed=true) AST, $this->parseAndPrint('array{}')); } @@ -105,13 +93,11 @@ public function testTrailingCommaIsAllowed(): void self::assertSame(<<<'AST' NamedTypeNode Name(array) - Identifier(array) - Shape\FieldsListNode(sealed) - Shape\NamedFieldNode(required) + Shape\FieldsListNode(isSealed=true) + Shape\NamedFieldNode(isOptional=false) Identifier(a) NamedTypeNode Name(int) - Identifier(int) AST, $this->parseAndPrint('array{a: int,}')); } @@ -120,13 +106,11 @@ public function testOptionalKey(): void self::assertSame(<<<'AST' NamedTypeNode Name(array) - Identifier(array) - Shape\FieldsListNode(sealed) - Shape\NamedFieldNode(optional) + Shape\FieldsListNode(isSealed=true) + Shape\NamedFieldNode(isOptional=true) Identifier(key) NamedTypeNode Name(Type) - Identifier(Type) AST, $this->parseAndPrint('array{key?: Type}')); } @@ -135,13 +119,11 @@ public function testUnsealedShape(): void self::assertSame(<<<'AST' NamedTypeNode Name(array) - Identifier(array) - Shape\FieldsListNode(unsealed) - Shape\NamedFieldNode(required) + Shape\FieldsListNode(isSealed=false) + Shape\NamedFieldNode(isOptional=false) Identifier(key) NamedTypeNode Name(type) - Identifier(type) AST, $this->parseAndPrint('array{key: type, ...}')); } @@ -150,22 +132,18 @@ public function testTypedUnsealedShape(): void self::assertSame(<<<'AST' NamedTypeNode Name(array) - Identifier(array) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(string) - Identifier(string) Template\TemplateArgumentNode NamedTypeNode Name(object) - Identifier(object) - Shape\FieldsListNode(unsealed) - Shape\NamedFieldNode(required) + Shape\FieldsListNode(isSealed=false) + Shape\NamedFieldNode(isOptional=false) Identifier(user) NamedTypeNode Name(User) - Identifier(User) AST, $this->parseAndPrint('array{user: User, ...}')); } @@ -174,15 +152,11 @@ public function testShapeOnArbitraryTypeName(): void self::assertSame(<<<'AST' NamedTypeNode Name(App\Domain\User) - Identifier(App) - Identifier(Domain) - Identifier(User) - Shape\FieldsListNode(sealed) - Shape\NamedFieldNode(required) + Shape\FieldsListNode(isSealed=true) + Shape\NamedFieldNode(isOptional=false) Identifier(userName) NamedTypeNode Name(non-empty-string) - Identifier(non-empty-string) AST, $this->parseAndPrint('App\\Domain\\User{userName: non-empty-string}')); } @@ -202,8 +176,63 @@ public function testDuplicateKeyIsNotAllowed(): void public function testOptionalValueSyntaxIsNotAllowed(): void { - $this->expectParsingException('unexpected "?"'); + $this->expectParsingException('a shape must be closed with a brace "}"'); $this->parse('array{key: Type?}'); } + + /** + * A "true" and a "null" name a field as the words they are written with, + * not as the values they name elsewhere. + * + * @return iterable + */ + public static function keywordKeyDataProvider(): iterable + { + yield 'true' => ['array{true: int}']; + yield 'false' => ['array{false: int}']; + yield 'null' => ['array{null: int}']; + } + + #[DataProvider('keywordKeyDataProvider')] + public function testKeywordKeyIsAName(string $type): void + { + self::assertSame($type, (new \TypeLang\Printer\PrettyTypePrinter())->print($this->parse($type))); + } + + /** + * A key is a number or a string and nothing else, the way a key of an + * array is. + * + * @return iterable + */ + public static function invalidKeyDataProvider(): iterable + { + yield 'boolean' => ['array{(true): int}']; + yield 'boolean of the other kind' => ['array{(false): int}']; + yield 'null' => ['array{(null): int}']; + yield 'float' => ['array{0.42: int}']; + yield 'float in parentheses' => ['array{(0.42): int}']; + yield 'variable' => ['array{$this: int}']; + yield 'union' => ['array{(A|B): int}']; + } + + #[DataProvider('invalidKeyDataProvider')] + public function testKeyIsANumberAStringOrANameAlone(string $type): void + { + $this->expectParsingException('Shape key must be a name, a number, a string'); + + $this->parse($type); + } + + /** + * A key ends in the ":" its value begins after, so a type that carries + * a colon of its own is no key. + */ + public function testKeyDoesNotReachBeyondAPrimaryType(): void + { + $this->expectParsingException('a shape must be closed with a brace "}"'); + + $this->parse('array{T is A ? B : C: int}'); + } } diff --git a/libs/parser/tests/Syntax/SyntaxTestCase.php b/libs/parser/tests/Syntax/SyntaxTestCase.php index bd876cd..616d49e 100644 --- a/libs/parser/tests/Syntax/SyntaxTestCase.php +++ b/libs/parser/tests/Syntax/SyntaxTestCase.php @@ -6,7 +6,7 @@ use PHPUnit\Framework\Attributes\Group; use TypeLang\Parser\Exception\FeatureNotAllowedException; -use TypeLang\Parser\Exception\ParseException; +use TypeLang\Parser\Exception\ParserException; use TypeLang\Parser\Exception\ParserExceptionInterface; use TypeLang\Parser\Exception\SemanticException; use TypeLang\Parser\Tests\TestCase; @@ -22,13 +22,13 @@ private function expectParserExceptionOf(string $class, ?string $message = null) $this->expectException($class); if ($message !== null) { - $this->expectExceptionMessageIsOrContains($message); + $this->expectExceptionMessage($message); } } protected function expectParsingException(?string $message = null): void { - $this->expectParserExceptionOf(ParseException::class, $message); + $this->expectParserExceptionOf(ParserException::class, $message); } protected function expectSemanticException(?string $message = null): void diff --git a/libs/parser/tests/Syntax/TemplateParameterTest.php b/libs/parser/tests/Syntax/TemplateParameterTest.php new file mode 100644 index 0000000..25ccc78 --- /dev/null +++ b/libs/parser/tests/Syntax/TemplateParameterTest.php @@ -0,0 +1,262 @@ +parseAndPrint('callable(T): T')); + } + + public function testEveryLimitIsReadIntoAPlaceOfItsOwn(): void + { + self::assertSame(<<<'AST' + CallableTypeNode + Name(Closure) + Callable\CallableParameterListNode + Template\TemplateParameterListNode + Template\TemplateParameterNode + Identifier(T) + Template\TemplateBoundEdgeNode + Identifier(of) + NamedTypeNode + Name(Some) + Template\TemplateBoundEdgeNode + Identifier(super) + NamedTypeNode + Name(Any) + NamedTypeNode + Name(int) + AST, $this->parseAndPrint('Closure()')); + } + + /** + * The word an upper bound is written with is kept as it is written, since + * an "of" and an "as" mean the same and only one of them was typed. + */ + public function testUpperBoundKeepsTheWordItIsWrittenWith(): void + { + $node = $this->parse('Closure()'); + + self::assertInstanceOf(CallableTypeNode::class, $node); + self::assertNotNull($node->templates); + self::assertSame('as', $node->templates->items[0]->upper?->operator->value); + } + + /** + * @return iterable + */ + public static function limitDataProvider(): iterable + { + yield 'of' => ['callable(): void', 'upper']; + yield 'as' => ['callable(): void', 'upper']; + yield 'super' => ['callable(): void', 'lower']; + yield 'assign' => ['callable(): void', 'default']; + } + + /** + * @param non-empty-string $type + * @param 'upper'|'lower'|'default' $filled + * @throws \Throwable + */ + #[DataProvider('limitDataProvider')] + public function testEveryLimitFillsThePlaceItBelongsTo(string $type, string $filled): void + { + $node = $this->parse($type); + + self::assertInstanceOf(CallableTypeNode::class, $node); + self::assertNotNull($node->templates); + + $parameter = $node->templates->items[0]; + + self::assertSame('T', $parameter->name->value); + + foreach (['upper', 'lower', 'default'] as $place) { + if ($place === $filled) { + self::assertNotNull($parameter->{$place}, "The {$place} must be filled"); + + continue; + } + + self::assertNull($parameter->{$place}, "The {$place} must be left out"); + } + } + + public function testASingleParameterCarriesEveryLimitItIsWrittenWith(): void + { + $node = $this->parse('callable(): void'); + + self::assertInstanceOf(CallableTypeNode::class, $node); + self::assertNotNull($node->templates); + + $parameter = $node->templates->items[0]; + + self::assertSame('of', $parameter->upper?->operator->value); + self::assertSame('super', $parameter->lower?->operator->value); + self::assertNotNull($parameter->default); + } + + public function testAParameterWrittenWithNoLimitCarriesNone(): void + { + $node = $this->parse('callable(): void'); + + self::assertInstanceOf(CallableTypeNode::class, $node); + self::assertNotNull($node->templates); + $parameter = $node->templates->items[0]; + + self::assertNull($parameter->upper); + self::assertNull($parameter->lower); + self::assertNull($parameter->default); + } + + /** + * @return iterable + */ + public static function invalidLimitDataProvider(): iterable + { + yield 'unknown word' => [ + 'callable(): void', + 'cannot be bounded with "whatever"', + ]; + yield 'an upper bound of another case' => [ + 'callable(): void', + 'cannot be bounded with "OF"', + ]; + yield 'a lower bound of another case' => [ + 'callable(): void', + 'cannot be bounded with "SUPER"', + ]; + yield 'two upper bounds' => [ + 'callable(): void', + 'cannot have more than one upper bound', + ]; + yield 'two lower bounds' => [ + 'callable(): void', + 'cannot have more than one lower bound', + ]; + yield 'two defaults' => [ + 'callable(): void', + 'cannot have more than one default', + ]; + yield 'upper bound behind the default' => [ + 'callable(): void', + 'default must be written last', + ]; + yield 'lower bound behind the default' => [ + 'callable(): void', + 'default must be written last', + ]; + } + + /** + * The two ends are told apart by the word each is written with, so which + * of them comes first says nothing. + */ + public function testTheTwoEndsAreWrittenInEitherOrder(): void + { + $node = $this->parse('callable(): void'); + + self::assertInstanceOf(CallableTypeNode::class, $node); + self::assertNotNull($node->templates); + + $parameter = $node->templates->items[0]; + + self::assertSame('of', $parameter->upper?->operator->value); + self::assertSame('super', $parameter->lower?->operator->value); + } + + /** + * @param non-empty-string $type + * @param non-empty-string $message + * @throws \Throwable + */ + #[DataProvider('invalidLimitDataProvider')] + public function testALimitIsWrittenAtMostOnceAndBoundsSomething(string $type, string $message): void + { + $this->expectParsingException($message); + + $this->parse($type); + } + + public function testSeveralParametersAreRead(): void + { + $node = $this->parse('callable(T, U): V'); + + self::assertInstanceOf(CallableTypeNode::class, $node); + self::assertNotNull($node->templates); + + $names = []; + + foreach ($node->templates as $parameter) { + $names[] = $parameter->name->value; + } + + self::assertSame(['T', 'U', 'V'], $names); + } + + public function testTrailingCommaIsAllowed(): void + { + $node = $this->parse('callable(): void'); + + self::assertInstanceOf(CallableTypeNode::class, $node); + self::assertNotNull($node->templates); + self::assertCount(2, $node->templates); + } + + /** + * A "<...>" that no parenthesis follows is an argument list, and an + * argument list describes no bounds. + */ + public function testBoundsBelongToACallableAlone(): void + { + $this->expectParsingException(); + + $this->parse('Collection'); + } + + public function testATypeUsedWithArgumentsIsNotACallable(): void + { + $node = $this->parse('Collection'); + + self::assertInstanceOf(\TypeLang\Type\NamedTypeNode::class, $node); + self::assertNotNull($node->arguments); + } + + public function testParametersAreRefusedWhenGenericsAreDisabled(): void + { + $this->expectParsingException('Template parameters not allowed'); + + $this->parse('callable(T): T', ['generics' => false]); + } + + public function testParametersAreRefusedWhenCallablesAreDisabled(): void + { + $this->expectParsingException('Callable types not allowed'); + + $this->parse('callable(T): T', ['callables' => false]); + } +} diff --git a/libs/parser/tests/Syntax/TriviaTest.php b/libs/parser/tests/Syntax/TriviaTest.php new file mode 100644 index 0000000..c7b7235 --- /dev/null +++ b/libs/parser/tests/Syntax/TriviaTest.php @@ -0,0 +1,254 @@ + + */ + public static function commentedUnionDataProvider(): iterable + { + yield 'block comment' => ['int /* c */ | string']; + yield 'two block comments' => ['int /* a */ /* b */ | string']; + yield 'line comment' => ["int // c\n| string"]; + yield 'hash comment' => ["int # c\n| string"]; + yield 'empty hash comment' => ["int #\n| string"]; + yield 'comment behind the delimiter' => ['int|/* c */string']; + yield 'comment on either side' => ['/* a */int|string/* b */']; + } + + /** + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('commentedUnionDataProvider')] + public function testACommentPartsTwoTokens(string $type): void + { + self::assertSame(<<<'AST' + UnionTypeNode + NamedTypeNode + Name(int) + NamedTypeNode + Name(string) + AST, $this->parseAndPrint($type)); + } + + /** + * A comment ends the line it is written on, so whatever follows the line + * is read as usual. + */ + public function testALineCommentEndsWithItsLine(): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(array) + Shape\FieldsListNode(isSealed=true) + Shape\NamedFieldNode(isOptional=false) + Identifier(a) + NamedTypeNode + Name(int) + AST, $this->parseAndPrint("array{\n // the key\n a: int,\n}")); + } + + /** + * @return iterable + */ + public static function commentedNameDataProvider(): iterable + { + yield 'in front of the separator' => ['Some/* c */\Any']; + yield 'behind the separator' => ['Some\/* c */Any']; + } + + /** + * A separator parts the segments of a name on its own, so a comment + * standing beside it parts nothing. + * + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('commentedNameDataProvider')] + public function testACommentStandsInsideAName(string $type): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(Some\Any) + AST, $this->parseAndPrint($type)); + } + + public function testACommentStandsInsideAnEmptyShape(): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(array) + Shape\FieldsListNode(isSealed=true) + AST, $this->parseAndPrint('array{ /* c */ }')); + } + + public function testACommentStandsBetweenANameAndItsParameters(): void + { + self::assertSame(<<<'AST' + CallableTypeNode + Name(callable) + Callable\CallableParameterListNode + NamedTypeNode + Name(void) + AST, $this->parseAndPrint('callable/* c */(): void')); + } + + /** + * @return iterable + */ + public static function spacedTypeDataProvider(): iterable + { + yield 'around the arguments' => ['Some < int , string >']; + yield 'no space at all' => ['Some']; + } + + /** + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('spacedTypeDataProvider')] + public function testSpaceAroundATokenIsOfNoMeaning(string $type): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(Some) + Template\TemplateArgumentListNode + Template\TemplateArgumentNode + NamedTypeNode + Name(int) + Template\TemplateArgumentNode + NamedTypeNode + Name(string) + AST, $this->parseAndPrint($type)); + } + + public function testSpaceStandsAroundEveryPartOfACallable(): void + { + self::assertSame(<<<'AST' + CallableTypeNode + Name(callable) + Callable\CallableParameterListNode + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) + NamedTypeNode + Name(int) + NamedTypeNode + Name(void) + AST, $this->parseAndPrint('callable ( int ) : void')); + } + + public function testATypeIsSurroundedByBlankLines(): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(int) + AST, $this->parseAndPrint("\n\n int \n\n")); + } + + /** + * The two words of an "is not" are two tokens, so whatever stands between + * any other pair of tokens stands between them as well. + * + * @return iterable + */ + public static function partedIsNotDataProvider(): iterable + { + yield 'a single space' => ['A is not B ? C : D']; + yield 'several spaces' => ['A is not B ? C : D']; + yield 'a tabulation' => ["A is\tnot B ? C : D"]; + yield 'a line terminator' => ["A is\nnot B ? C : D"]; + yield 'a block comment' => ['A is/* c */not B ? C : D']; + yield 'a line comment' => ["A is // c\nnot B ? C : D"]; + yield 'a comment and a line terminator' => ["A is /* c */\n not B ? C : D"]; + } + + /** + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('partedIsNotDataProvider')] + public function testTriviaPartsTheWordsOfAnIsNot(string $type): void + { + self::assertSame(<<<'AST' + TernaryExpressionNode + Condition\NotEqualConditionNode + NamedTypeNode + Name(A) + NamedTypeNode + Name(B) + NamedTypeNode + Name(C) + NamedTypeNode + Name(D) + AST, $this->parseAndPrint($type)); + } + + /** + * The words are read the way they are written, so a single word that is + * spelled like the two of them is an ordinary name. + */ + public function testTheWordsOfAnIsNotAreTwoWords(): void + { + $this->expectParsingException(); + + $this->parse('A isnot B ? C : D'); + } + + /** + * @return iterable + */ + public static function casedIsNotDataProvider(): iterable + { + yield 'uppercase' => ['A IS NOT B ? C : D']; + yield 'uppercase negation alone' => ['A is NOT B ? C : D']; + yield 'mixed case' => ['A Is NoT B ? C : D']; + } + + /** + * The words are written the one way they are written, so a word of any + * other case is an ordinary name and opens no condition. + * + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('casedIsNotDataProvider')] + public function testTheWordsOfAnIsNotAreCaseSensitive(string $type): void + { + $this->expectParsingException(); + + $this->parse($type); + } + + public function testAnUnterminatedBlockCommentIsRefused(): void + { + $this->expectParsingException('unexpected "/*"'); + + $this->parse('/* the comment that never ends'); + } + + public function testACommentAloneIsNoType(): void + { + $this->expectParsingException('unexpected end of input'); + + $this->parse('// only a comment'); + } + + public function testSpaceAloneIsNoType(): void + { + $this->expectParsingException('unexpected end of input'); + + $this->parse(" \n\t "); + } +} diff --git a/libs/parser/tests/Syntax/VariableTest.php b/libs/parser/tests/Syntax/VariableTest.php new file mode 100644 index 0000000..2b35b54 --- /dev/null +++ b/libs/parser/tests/Syntax/VariableTest.php @@ -0,0 +1,269 @@ +parseAndPrint('$this')); + } + + public function testThisStandsAsAReturnType(): void + { + self::assertSame(<<<'AST' + CallableTypeNode + Name(callable) + Callable\CallableParameterListNode + ThisNode + AST, $this->parseAndPrint('callable(): $this')); + } + + public function testThisStandsAsAParameterType(): void + { + self::assertSame(<<<'AST' + CallableTypeNode + Name(callable) + Callable\CallableParameterListNode + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) + ThisNode + NamedTypeNode + Name(void) + AST, $this->parseAndPrint('callable($this): void')); + } + + /** + * The same word names a parameter once a type stands in front of it, so + * it is read as a variable and not as a type. + */ + public function testThisNamesAParameter(): void + { + self::assertSame(<<<'AST' + CallableTypeNode + Name(callable) + Callable\CallableParameterListNode + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) + NamedTypeNode + Name(int) + VariableNode + Identifier(this) + NamedTypeNode + Name(void) + AST, $this->parseAndPrint('callable(int $this): void')); + } + + public function testThisIsATemplateArgument(): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(Some) + Template\TemplateArgumentListNode + Template\TemplateArgumentNode + ThisNode + AST, $this->parseAndPrint('Some<$this>')); + } + + public function testThisIsAShapeValue(): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(array) + Shape\FieldsListNode(isSealed=true) + Shape\NamedFieldNode(isOptional=false) + Identifier(a) + ThisNode + AST, $this->parseAndPrint('array{a: $this}')); + } + + /** + * @return iterable + */ + public static function wrappedThisDataProvider(): iterable + { + yield 'list' => ['$this[]', 'TypesListNode']; + yield 'nullable' => ['?$this', 'NullableTypeNode']; + yield 'union' => ['$this|null', 'UnionTypeNode']; + } + + /** + * @param non-empty-string $type + * @param non-empty-string $wrapper + * @throws \Throwable + */ + #[DataProvider('wrappedThisDataProvider')] + public function testThisIsWrappedLikeAnyOtherType(string $type, string $wrapper): void + { + self::assertStringStartsWith($wrapper, $this->parseAndPrint($type)); + } + + /** + * A condition reads its subject as a type first, so the word is the type + * and not the variable it is spelled like. + */ + public function testThisStandsAsATypeOnTheLeftOfACondition(): void + { + $node = $this->parse('$this is B ? C : D'); + + self::assertInstanceOf(TernaryExpressionNode::class, $node); + self::assertInstanceOf(ThisNode::class, $node->condition->subject); + } + + /** + * The token is written the one way it is written, so a word that only + * looks like it is an ordinary variable. + */ + public function testTheThisTokenIsCaseSensitive(): void + { + $node = $this->parse('$This is B ? C : D'); + + self::assertInstanceOf(TernaryExpressionNode::class, $node); + self::assertInstanceOf(VariableNode::class, $node->condition->subject); + self::assertSame('This', $node->condition->subject->name->value); + } + + public function testAVariableStandsBesideACondition(): void + { + $node = $this->parse('$value is B ? C : D'); + + self::assertInstanceOf(TernaryExpressionNode::class, $node); + self::assertInstanceOf(VariableNode::class, $node->condition->subject); + self::assertSame('value', $node->condition->subject->name->value); + } + + /** + * A variable names a place a value is kept in, so it is no type and + * stands nowhere a type is expected. + */ + public function testAVariableIsNoTypeOfItsOwn(): void + { + $this->expectParsingException(); + + $this->parse('$value'); + } + + public function testAVariableIsNoTemplateArgument(): void + { + $this->expectParsingException(); + + $this->parse('Some<$value>'); + } + + /** + * A variable carries no type the rest of the reading could go on with, + * so it stands nowhere a type is wrapped, joined or indexed. The subject + * of a condition is the one place a variable is read. + * + * @return iterable + */ + public static function variableBesideATypeDataProvider(): iterable + { + yield 'a nullable' => ['?$value is B ? C : D']; + yield 'a union' => ['int|$value is B ? C : D']; + yield 'an intersection' => ['int&$value is B ? C : D']; + yield 'a union it opens' => ['$value|int is B ? C : D']; + yield 'a list' => ['$value[] is B ? C : D']; + yield 'an offset' => ['$value[0] is B ? C : D']; + yield 'a group' => ['($value)']; + yield 'a shape value' => ['array{a: $value}']; + yield 'a return type' => ['callable(): $value']; + } + + /** + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('variableBesideATypeDataProvider')] + public function testAVariableStandsInNoTypePosition(string $type): void + { + // The token is reported the way any other unexpected one is, rather + // than raised as an error of the parser itself. + $this->expectException(UnexpectedTokenException::class); + + $this->parse($type); + } + + public function testThisCarriesNoClassConstant(): void + { + $this->expectParsingException('unexpected "::"'); + + $this->parse('$this::CONST'); + } + + public function testAParameterNameStandsBehindItsTypeAlone(): void + { + $this->expectParsingException(); + + $this->parse('callable($value int): void'); + } + + /** + * A dash belongs to a name and not to a variable, the way it does not + * belong to one in PHP either. + * + * @return iterable + */ + public static function dashedVariableDataProvider(): iterable + { + yield 'an ordinary variable' => ['$value-of is B ? C : D']; + yield 'the this variable' => ['$this-of is B ? C : D']; + yield 'a parameter name' => ['callable(int $value-of): void']; + } + + /** + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('dashedVariableDataProvider')] + public function testAVariableCarriesNoDash(string $type): void + { + $this->expectParsingException(); + + $this->parse($type); + } + + /** + * @return iterable + */ + public static function variableBodyDataProvider(): iterable + { + yield 'an underscore' => ['$value_1']; + yield 'a leading underscore' => ['$_value']; + yield 'digits' => ['$value42']; + yield 'a word that opens with this' => ['$thisValue']; + } + + /** + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('variableBodyDataProvider')] + public function testAVariableCarriesWhateverANameCarriesBesideADash(string $type): void + { + $node = $this->parse($type . ' is B ? C : D'); + + self::assertInstanceOf(TernaryExpressionNode::class, $node); + self::assertInstanceOf(VariableNode::class, $node->condition->subject); + self::assertSame(\substr($type, 1), $node->condition->subject->name->value); + } + public function testTheDollarSignAloneIsNoVariable(): void + { + $this->expectParsingException(); + + $this->parse('$'); + } +} diff --git a/libs/parser/tests/Syntax/WildcardTest.php b/libs/parser/tests/Syntax/WildcardTest.php new file mode 100644 index 0000000..5271f2d --- /dev/null +++ b/libs/parser/tests/Syntax/WildcardTest.php @@ -0,0 +1,98 @@ +parseAndPrint('Collection<*>')); + } + + public function testWildcardBesideAType(): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(HashMap) + Template\TemplateArgumentListNode + Template\TemplateArgumentNode + NamedTypeNode + Name(array-key) + Template\TemplateArgumentNode + WildcardNode(*) + AST, $this->parseAndPrint('HashMap')); + } + + public function testWildcardCarriesAHint(): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(Collection) + Template\TemplateArgumentListNode + Template\TemplateArgumentNode + WildcardNode(*) + Identifier(out) + AST, $this->parseAndPrint('Collection')); + } + + public function testWildcardIsReadInsideAnUnsealedShape(): void + { + self::assertSame(<<<'AST' + NamedTypeNode + Name(array) + Template\TemplateArgumentListNode + Template\TemplateArgumentNode + WildcardNode(*) + Shape\FieldsListNode(isSealed=false) + AST, $this->parseAndPrint('array{...<*>}')); + } + + public function testWildcardOffsetIsTheOneItIsWrittenAt(): void + { + $node = $this->parse('Collection'); + + self::assertInstanceOf(\TypeLang\Type\NamedTypeNode::class, $node); + self::assertNotNull($node->arguments); + + $argument = $node->arguments->items[1]; + + self::assertInstanceOf(\TypeLang\Type\WildcardNode::class, $argument->value); + self::assertSame(16, $argument->value->offset); + } + + public function testWildcardIsNotAType(): void + { + $this->expectParsingException(); + + $this->parse('int|*'); + } + + public function testWildcardIsNotACallableParameter(): void + { + $this->expectParsingException(); + + $this->parse('callable(*): void'); + } + + public function testWildcardIsRefusedWhenGenericsAreDisabled(): void + { + $this->expectParsingException('Template arguments not allowed'); + + $this->parse('Collection<*>', ['generics' => false]); + } +} diff --git a/libs/parser/tests/TestCase.php b/libs/parser/tests/TestCase.php index c93689e..ff3f499 100644 --- a/libs/parser/tests/TestCase.php +++ b/libs/parser/tests/TestCase.php @@ -7,8 +7,10 @@ use JetBrains\PhpStorm\Language; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase as BaseTestCase; -use TypeLang\Parser\ParsedResult; +use TypeLang\Parser\Partial\ParsedResult; +use TypeLang\Parser\Partial\SuccessfulParsedResult; use TypeLang\Parser\Traverser; +use TypeLang\Parser\Validation\CheckResult; use TypeLang\Parser\TypeParser; use TypeLang\Parser\TypeParserFeatures; use TypeLang\Parser\TypeParserInterface; @@ -27,15 +29,12 @@ * list?: bool, * offsets?: bool, * hints?: bool, - * attributes?: bool, * } */ #[Group('unit'), Group('type-lang/parser')] abstract class TestCase extends BaseTestCase { - protected TypeParserInterface $parser { - get => $this->parser ??= new TypeParser(); - } + private ?TypeParserInterface $parser = null; /** * @param ParserOptionsType $options @@ -43,7 +42,7 @@ abstract class TestCase extends BaseTestCase protected function parser(array $options = []): TypeParserInterface { if ($options === []) { - return $this->parser; + return $this->parser ??= new TypeParser(); } return new TypeParser(new TypeParserFeatures(...$options)); @@ -64,11 +63,22 @@ protected function parse(#[Language('PHP')] string $code, array $options = []): * @param ParserOptionsType $options * @throws \Throwable */ - protected function parseTolerant(#[Language('PHP')] string $code, array $options = []): ParsedResult + protected function partial(#[Language('PHP')] string $code, array $options = []): ParsedResult { $parser = $this->parser($options); - return $parser->parseTolerant($code); + return $parser->partial($code); + } + + /** + * @param ParserOptionsType $options + * @throws \Throwable + */ + protected function validate(#[Language('PHP')] string $code, array $options = []): CheckResult + { + $parser = $this->parser($options); + + return $parser->validate($code); } protected function print(TypeNode $statement): string @@ -96,7 +106,9 @@ protected function parseAndPrint(#[Language('PHP')] string $code, array $options */ protected function tolerantParseAndPrint(#[Language('PHP')] string $code, array $options = []): string { - $result = $this->parseTolerant($code, $options); + $result = $this->partial($code, $options); + + self::assertInstanceOf(SuccessfulParsedResult::class, $result); return $this->print($result->type); } diff --git a/libs/parser/tests/Traverser/MatcherVisitorTest.php b/libs/parser/tests/Traverser/MatcherVisitorTest.php new file mode 100644 index 0000000..a504780 --- /dev/null +++ b/libs/parser/tests/Traverser/MatcherVisitorTest.php @@ -0,0 +1,225 @@ + true); + + self::assertFalse($visitor->hasMatches()); + self::assertNull($visitor->node); + } + + #[Test] + public function theMatchedNodeIsAvailableAfterTheTraversal(): void + { + $expected = $this->type(); + + $visitor = Traverser::through( + new MatcherVisitor(static fn(Node $node): bool => $node instanceof NamedTypeNode), + [$expected], + ); + + self::assertTrue($visitor->hasMatches()); + self::assertSame($expected, $visitor->node); + } + + #[Test] + public function nothingIsFoundInCaseOfNoNodeMatches(): void + { + $visitor = Traverser::through( + new MatcherVisitor(static fn(Node $node): bool => $node instanceof UnionTypeNode), + [$this->type()], + ); + + self::assertFalse($visitor->hasMatches()); + self::assertNull($visitor->node); + } + + #[Test] + public function theFirstMatchingNodeIsReturned(): void + { + $first = $this->type('First'); + $second = $this->type('Second'); + + $visitor = Traverser::through( + new MatcherVisitor(static fn(Node $node): bool => $node instanceof NamedTypeNode), + [$first, $second], + ); + + self::assertSame($first, $visitor->node); + } + + #[Test] + public function nestedNodesAreMatched(): void + { + $visitor = Traverser::through( + new MatcherVisitor(static fn(Node $node): bool => $node instanceof Name), + [$this->type('Foo\\Bar')], + ); + + self::assertInstanceOf(Name::class, $visitor->node); + self::assertSame('Foo\\Bar', $visitor->node->toString()); + } + + #[Test] + public function theBreakConditionStopsTheSearch(): void + { + $visitor = Traverser::through( + new MatcherVisitor( + matcher: static fn(Node $node): bool => $node instanceof Name, + break: static fn(Node $node): bool => $node instanceof NamedTypeNode, + ), + [$this->type('Foo\\Bar')], + ); + + self::assertFalse($visitor->hasMatches()); + } + + #[Test] + public function theMatcherHasAPriorityOverTheBreakCondition(): void + { + $expected = $this->type(); + + $visitor = Traverser::through( + new MatcherVisitor( + matcher: static fn(Node $node): bool => $node instanceof NamedTypeNode, + break: static fn(Node $node): bool => $node instanceof NamedTypeNode, + ), + [$expected], + ); + + self::assertSame($expected, $visitor->node); + } + + #[Test] + public function theBreakConditionStopsTheWholeTraversal(): void + { + $visitor = Traverser::through( + new MatcherVisitor( + matcher: static fn(Node $node): bool => $node instanceof Identifier, + break: static fn(Node $node): bool => $node instanceof UnionTypeNode, + ), + [ + new UnionTypeNode([$this->type('A'), $this->type('B')]), + new Identifier('Example'), + ], + ); + + self::assertFalse($visitor->hasMatches()); + } + + #[Test] + public function enterSkipsTheChildrenOfTheMatchedNode(): void + { + $visitor = new MatcherVisitor(static fn(Node $node): bool => true); + + self::assertSame(Command::SkipChildren, $visitor->enter($this->type())); + } + + #[Test] + public function enterDescendsIntoTheNonMatchingNode(): void + { + $visitor = new MatcherVisitor(static fn(Node $node): bool => false); + + self::assertNull($visitor->enter($this->type())); + } + + #[Test] + public function beforeResetsThePreviouslyMatchedNode(): void + { + $visitor = Traverser::through( + new MatcherVisitor(static fn(Node $node): bool => true), + [$this->type()], + ); + + $visitor->before(); + + self::assertFalse($visitor->hasMatches()); + self::assertNull($visitor->node); + } + + #[Test] + public function theSameVisitorCanBeUsedSeveralTimes(): void + { + $visitor = new MatcherVisitor(static fn(Node $node): bool => $node instanceof NamedTypeNode); + $traverser = Traverser::new([$visitor]); + + $traverser->traverse([$this->type('First')]); + $expected = $this->type('Second'); + $traverser->traverse([$expected]); + + self::assertTrue($visitor->hasMatches()); + self::assertSame($expected, $visitor->node); + } + + #[Test] + public function theBreakConditionDoesNotAffectTheNextTraversal(): void + { + $visitor = new MatcherVisitor( + matcher: static fn(Node $node): bool => $node instanceof Name, + break: static fn(Node $node): bool => $node instanceof UnionTypeNode, + ); + + $traverser = Traverser::new([$visitor]); + + $traverser->traverse([new UnionTypeNode([$this->type('A'), $this->type('B')])]); + $traverser->traverse([$this->type('Foo\\Bar')]); + + self::assertTrue($visitor->hasMatches()); + } + + #[Test] + public function theMatcherReceivesEveryVisitedNode(): void + { + $visited = []; + + Traverser::through( + new MatcherVisitor(static function (Node $node) use (&$visited): bool { + $visited[] = $node::class; + + return false; + }), + [$this->type('Foo\\Bar')], + ); + + self::assertSame([ + NamedTypeNode::class, + Name::class, + ], $visited); + } + + #[Test] + public function theMatcherCanBeAnArbitraryCondition(): void + { + $visitor = Traverser::through( + new MatcherVisitor(static fn(Node $node): bool + => $node instanceof NamedTypeNode && $node->name->toString() === 'Second'), + [$this->type('First'), $this->type('Second')], + ); + + self::assertInstanceOf(NamedTypeNode::class, $visitor->node); + self::assertSame('Second', $visitor->node->name->toString()); + } +} diff --git a/libs/parser/tests/Traverser/StringDumperVisitorTest.php b/libs/parser/tests/Traverser/StringDumperVisitorTest.php new file mode 100644 index 0000000..4c80ec2 --- /dev/null +++ b/libs/parser/tests/Traverser/StringDumperVisitorTest.php @@ -0,0 +1,238 @@ + $nodes + */ + private function dump(iterable $nodes, ?string $namespace = null): string + { + $visitor = $namespace === null + ? new StringDumperVisitor() + : new StringDumperVisitor($namespace); + + Traverser::new([$visitor])->traverse($nodes); + + return $visitor->output; + } + + #[Test] + public function theOutputIsEmptyBeforeTheTraversal(): void + { + self::assertSame('', (new StringDumperVisitor())->output); + } + + #[Test] + public function theOutputIsEmptyInCaseOfNoNodes(): void + { + self::assertSame('', $this->dump([])); + } + + #[Test] + public function theNodeNameIsPrintedWithoutTheSimplifiedNamespace(): void + { + self::assertSame( + "NamedTypeNode\n Name(Foo\\Bar)\n", + $this->dump([new NamedTypeNode(Name::createFromString('Foo\\Bar'))]), + ); + } + + #[Test] + public function theSimplifiedNamespaceCanBeChanged(): void + { + self::assertSame( + "Type\\Identifier(Example)\n", + $this->dump([new Identifier('Example')], 'TypeLang\\'), + ); + } + + #[Test] + public function theNodeNameIsPrintedAsIsInCaseOfANonMatchingNamespace(): void + { + self::assertSame( + "TypeLang\\Type\\Identifier(Example)\n", + $this->dump([new Identifier('Example')], 'Example\\'), + ); + } + + #[Test] + public function theDefaultSimplifiedNamespaceIsUsed(): void + { + self::assertSame( + $this->dump([new Identifier('Example')]), + $this->dump([new Identifier('Example')], DumperVisitor::DEFAULT_SIMPLIFIED_NODE_NAMESPACE), + ); + } + + #[Test] + public function everyNestingLevelIsIndentedByTwoSpaces(): void + { + self::assertSame(<<<'OUTPUT' + UnionTypeNode + NamedTypeNode + Name(int) + NamedTypeNode + Name(string) + + OUTPUT, $this->dump([ + new UnionTypeNode([ + new NamedTypeNode(Name::createFromString('int')), + new NamedTypeNode(Name::createFromString('string')) + ]), + ])); + } + + #[Test] + public function theStringableNodeIsPrintedUsingItsStringValue(): void + { + self::assertSame( + "Name(\\Foo\\Bar)\n", + $this->dump([Name::createFromString('\\Foo\\Bar')]), + ); + } + + #[Test] + public function theNodeWithoutScalarPropertiesHasNoSuffix(): void + { + self::assertStringStartsWith( + "NamedTypeNode\n", + $this->dump([new NamedTypeNode(Name::createFromString('Example'))]), + ); + } + + #[Test] + public function theOffsetPropertyIsNotPrinted(): void + { + $node = new NamedTypeNode(Name::createFromString('Example')); + $node->offset = 42; + + self::assertStringStartsWith("NamedTypeNode\n", $this->dump([$node])); + } + + #[Test] + public function theWritableScalarPropertiesArePrinted(): void + { + $node = new class extends Node { + public bool $enabled = true; + + public int $count = 42; + + public string $title = 'example'; + }; + + self::assertStringEndsWith("(enabled=true, count=42, title='example')\n", $this->dump([$node])); + } + + #[Test] + public function theReadonlyPropertiesAreNotPrinted(): void + { + $node = new class extends Node { + public readonly bool $hidden; + + public bool $visible = true; + + public function __construct() + { + $this->hidden = true; + + parent::__construct(); + } + }; + + self::assertStringEndsWith("(visible=true)\n", $this->dump([$node])); + } + + #[Test] + public function theStaticPropertiesAreNotPrinted(): void + { + $node = new class extends Node { + public static bool $hidden = true; + + public bool $visible = true; + }; + + self::assertStringEndsWith("(visible=true)\n", $this->dump([$node])); + } + + #[Test] + public function theNonScalarPropertiesAreNotPrinted(): void + { + $node = new class extends Node { + /** + * @var list + */ + public array $items = []; + + public ?string $nothing = null; + + public bool $visible = true; + }; + + self::assertStringEndsWith("(visible=true)\n", $this->dump([$node])); + } + + #[Test] + public function theShapeFieldsArePrintedUsingTheirScalarProperties(): void + { + self::assertSame(<<<'OUTPUT' + NamedTypeNode + Name(array) + Shape\FieldsListNode(isSealed=true) + Shape\NamedFieldNode(isOptional=true) + Identifier(foo) + NamedTypeNode + Name(int) + + OUTPUT, $this->dump([$this->parse('array{foo?: int}')])); + } + + #[Test] + public function resetClearsTheOutput(): void + { + $visitor = new StringDumperVisitor(); + Traverser::new([$visitor])->traverse([new Identifier('Example')]); + + $visitor->reset(); + + self::assertSame('', $visitor->output); + } + + #[Test] + public function theOutputIsClearedOnEachTraversal(): void + { + $visitor = new StringDumperVisitor(); + $traverser = Traverser::new([$visitor]); + + $traverser->traverse([new Identifier('First')]); + $traverser->traverse([new Identifier('Second')]); + + self::assertSame("Identifier(Second)\n", $visitor->output); + } + + #[Test] + public function theIndentationIsClearedOnEachTraversal(): void + { + $visitor = new StringDumperVisitor(); + $traverser = Traverser::new([$visitor]); + + $traverser->traverse([new NamedTypeNode(Name::createFromString('First'))]); + $traverser->traverse([new Identifier('Second')]); + + self::assertSame("Identifier(Second)\n", $visitor->output); + } +} diff --git a/libs/parser/tests/Traverser/TraverserTest.php b/libs/parser/tests/Traverser/TraverserTest.php new file mode 100644 index 0000000..ce2b026 --- /dev/null +++ b/libs/parser/tests/Traverser/TraverserTest.php @@ -0,0 +1,401 @@ +, non-empty-string> + */ + private \ArrayObject $log; + + protected function setUp(): void + { + parent::setUp(); + + $this->log = new \ArrayObject(); + } + + private function type(string $name = 'Example'): NamedTypeNode + { + return new NamedTypeNode(Name::createFromString($name)); + } + + /** + * @param non-empty-string $label + * @return VisitorInterface&object{ + * entered: list>, + * left: list>, + * before: int<0, max>, + * after: int<0, max> + * } + */ + private function recorder(string $label = 'a', ?Command $command = null): VisitorInterface + { + return new class ($this->log, $label, $command) extends Visitor { + /** + * @var list> + */ + public array $entered = []; + + /** + * @var list> + */ + public array $left = []; + + /** + * @var int<0, max> + */ + public int $before = 0; + + /** + * @var int<0, max> + */ + public int $after = 0; + + /** + * @param \ArrayObject, non-empty-string> $log + * @param non-empty-string $label + */ + public function __construct( + private readonly \ArrayObject $log, + private readonly string $label, + private readonly ?Command $command = null, + ) {} + + public function before(): void + { + ++$this->before; + $this->log[] = $this->label . ':before'; + } + + public function enter(Node $node): ?Command + { + $this->entered[] = $node::class; + $this->log[] = $this->label . ':enter'; + + return $this->command; + } + + public function leave(Node $node): void + { + $this->left[] = $node::class; + $this->log[] = $this->label . ':leave'; + } + + public function after(): void + { + ++$this->after; + $this->log[] = $this->label . ':after'; + } + }; + } + + #[Test] + public function traverseVisitsEveryNodeInDepth(): void + { + $visitor = $this->recorder(); + + Traverser::new([$visitor])->traverse([$this->type('Foo\\Bar')]); + + self::assertSame([ + NamedTypeNode::class, + Name::class, + ], $visitor->entered); + } + + #[Test] + public function traverseLeavesEveryEnteredNode(): void + { + $visitor = $this->recorder(); + + Traverser::new([$visitor])->traverse([$this->type('Foo\\Bar')]); + + self::assertSame([ + Name::class, + NamedTypeNode::class, + ], $visitor->left); + } + + #[Test] + public function traverseCallsTheLifecycleHooksOnce(): void + { + $visitor = $this->recorder(); + + Traverser::new([$visitor])->traverse([$this->type()]); + + self::assertSame(1, $visitor->before); + self::assertSame(1, $visitor->after); + } + + #[Test] + public function beforeIsCalledPriorToAnyNodeAndAfterIsCalledLast(): void + { + Traverser::new([$this->recorder()])->traverse([$this->type()]); + + self::assertSame('a:before', $this->log[0]); + self::assertSame('a:after', $this->log[\count($this->log) - 1]); + } + + #[Test] + public function skipChildrenCommandStopsTheDescent(): void + { + $visitor = $this->recorder(command: Command::SkipChildren); + + Traverser::new([$visitor])->traverse([$this->type('Foo\\Bar')]); + + self::assertSame([NamedTypeNode::class], $visitor->entered); + } + + #[Test] + public function skipChildrenCommandDoesNotSkipTheLeaveCall(): void + { + $visitor = $this->recorder(command: Command::SkipChildren); + + Traverser::new([$visitor])->traverse([$this->type('Foo\\Bar')]); + + self::assertSame([NamedTypeNode::class], $visitor->left); + } + + #[Test] + public function commandContainsTheSkipChildrenCase(): void + { + self::assertSame([Command::SkipChildren], Command::cases()); + } + + #[Test] + public function throughReturnsThePassedVisitor(): void + { + $visitor = $this->recorder(); + + $result = Traverser::through($visitor, [$this->type()]); + + self::assertSame($visitor, $result); + } + + #[Test] + public function throughTraversesTheNodes(): void + { + $visitor = Traverser::through($this->recorder(), [$this->type('Foo\\Bar')]); + + self::assertSame([ + NamedTypeNode::class, + Name::class, + ], $visitor->entered); + } + + #[Test] + public function newCreatesAnEmptyTraverser(): void + { + Traverser::new()->traverse([$this->type()]); + + self::assertSame([], $this->log->getArrayCopy()); + } + + #[Test] + public function withReturnsANewInstance(): void + { + $traverser = Traverser::new(); + + self::assertNotSame($traverser, $traverser->with($this->recorder())); + } + + #[Test] + public function withDoesNotModifyTheOriginalTraverser(): void + { + $traverser = Traverser::new(); + $traverser->with($this->recorder()); + + $traverser->traverse([$this->type()]); + + self::assertSame([], $this->log->getArrayCopy()); + } + + #[Test] + public function withAppendsTheVisitorToTheEnd(): void + { + Traverser::new([$this->recorder('a')]) + ->with($this->recorder('b')) + ->traverse([]); + + self::assertSame([ + 'a:before', + 'b:before', + 'a:after', + 'b:after', + ], $this->log->getArrayCopy()); + } + + #[Test] + public function withPrependsTheVisitorToTheBeginning(): void + { + Traverser::new([$this->recorder('a')]) + ->with($this->recorder('b'), true) + ->traverse([]); + + self::assertSame([ + 'b:before', + 'a:before', + 'b:after', + 'a:after', + ], $this->log->getArrayCopy()); + } + + #[Test] + public function withPropertyAccessorReturnsANewInstance(): void + { + $traverser = Traverser::new(); + + self::assertNotSame( + $traverser, + $traverser->withPropertyAccessor(new SimplePropertyAccessor()), + ); + } + + #[Test] + public function withPropertyAccessorChangesTheTraversedChildren(): void + { + $accessor = new class implements PropertyAccessorInterface { + public function unwrap(object $object): iterable + { + return []; + } + }; + + $visitor = $this->recorder(); + + Traverser::new([$visitor]) + ->withPropertyAccessor($accessor) + ->traverse([$this->type('Foo\\Bar')]); + + self::assertSame([NamedTypeNode::class], $visitor->entered); + } + + #[Test] + public function withPropertyAccessorDoesNotModifyTheOriginalTraverser(): void + { + $accessor = new class implements PropertyAccessorInterface { + public function unwrap(object $object): iterable + { + return []; + } + }; + + $visitor = $this->recorder(); + $traverser = Traverser::new([$visitor]); + $traverser->withPropertyAccessor($accessor); + + $traverser->traverse([$this->type('Foo\\Bar')]); + + self::assertSame([ + NamedTypeNode::class, + Name::class, + ], $visitor->entered); + } + + #[Test] + public function traverseVisitsEveryNodeOfTheList(): void + { + $visitor = $this->recorder(); + + Traverser::new([$visitor])->traverse([$this->type('A'), $this->type('B')]); + + self::assertSame([ + NamedTypeNode::class, + Name::class, + NamedTypeNode::class, + Name::class, + ], $visitor->entered); + } + + #[Test] + public function traverseCanBeCalledSeveralTimes(): void + { + $visitor = $this->recorder(); + $traverser = Traverser::new([$visitor]); + + $traverser->traverse([$this->type()]); + $traverser->traverse([$this->type()]); + + self::assertSame(2, $visitor->before); + self::assertSame(2, $visitor->after); + } + + #[Test] + public function traverseVisitsTheNestedLogicalStatements(): void + { + $visitor = $this->recorder(); + + Traverser::new([$visitor])->traverse([ + new UnionTypeNode([$this->type('A'), $this->type('B')]), + ]); + + self::assertSame([ + UnionTypeNode::class, + NamedTypeNode::class, + Name::class, + NamedTypeNode::class, + Name::class, + ], $visitor->entered); + } + + #[Test] + public function traverseOfAnEmptyListCallsOnlyTheLifecycleHooks(): void + { + $visitor = $this->recorder(); + + Traverser::new([$visitor])->traverse([]); + + self::assertSame([], $visitor->entered); + self::assertSame(1, $visitor->before); + self::assertSame(1, $visitor->after); + } + + #[Test] + public function traverseAcceptsAnyTraversableSetOfNodes(): void + { + $visitor = $this->recorder(); + + Traverser::new([$visitor])->traverse(new \ArrayIterator([ + new Identifier('Example'), + ])); + + self::assertSame([Identifier::class], $visitor->entered); + } + + #[Test] + public function traverserCanBeCreatedUsingTheConstructor(): void + { + $visitor = $this->recorder(); + + (new Traverser([$visitor]))->traverse([$this->type()]); + + self::assertSame(1, $visitor->before); + } + + #[Test] + public function traverserConstructorAcceptsATraversableSetOfVisitors(): void + { + $visitor = $this->recorder(); + + (new Traverser(new \ArrayIterator([$visitor])))->traverse([$this->type()]); + + self::assertSame(1, $visitor->before); + } +} diff --git a/libs/parser/tests/Traverser/TypeMapVisitorTest.php b/libs/parser/tests/Traverser/TypeMapVisitorTest.php new file mode 100644 index 0000000..e1272a9 --- /dev/null +++ b/libs/parser/tests/Traverser/TypeMapVisitorTest.php @@ -0,0 +1,185 @@ + Name::createFromString($to)); + } + + #[Test] + public function theNameOfANamedTypeIsTransformed(): void + { + $node = new NamedTypeNode(Name::createFromString('Example')); + + Traverser::new([$this->alias()])->traverse([$node]); + + self::assertSame('Aliased', $node->name->toString()); + } + + #[Test] + public function theNameOfACallableTypeIsTransformed(): void + { + $node = new CallableTypeNode(Name::createFromString('callable')); + + Traverser::new([$this->alias()])->traverse([$node]); + + self::assertSame('Aliased', $node->name->toString()); + } + + #[Test] + public function theNamespaceOfAConstMaskIsTransformed(): void + { + $node = new ConstMaskNode( + new MaskNode([new Identifier('SOME_'), new WildcardNode()]), + Name::createFromString('Vendor'), + ); + + Traverser::new([$this->alias()])->traverse([$node]); + + self::assertInstanceOf(Name::class, $node->namespaceOrFullyQualified); + self::assertSame('Aliased', $node->namespaceOrFullyQualified->toString()); + } + + #[Test] + public function aConstMaskWithoutANamespaceIsLeftAlone(): void + { + $node = new ConstMaskNode(new MaskNode([new WildcardNode(), new Identifier('_SOME')])); + + Traverser::new([$this->alias()])->traverse([$node]); + + self::assertFalse($node->namespaceOrFullyQualified); + } + + #[Test] + public function theClassOfAClassConstIsTransformed(): void + { + $node = new ClassConstNode( + class: Name::createFromString('Example'), + constant: new Identifier('CONSTANT'), + ); + + Traverser::new([$this->alias()])->traverse([$node]); + + self::assertSame('Aliased', $node->class->toString()); + } + + #[Test] + public function theConstantOfAClassConstIsNotTransformed(): void + { + $node = new ClassConstNode( + class: Name::createFromString('Example'), + constant: new Identifier('CONSTANT'), + ); + + Traverser::new([$this->alias()])->traverse([$node]); + + self::assertSame('CONSTANT', $node->constant->toString()); + } + + #[Test] + public function theClassOfAClassConstMaskIsTransformed(): void + { + $node = new ClassConstMaskNode(Name::createFromString('Example')); + + Traverser::new([$this->alias()])->traverse([$node]); + + self::assertSame('Aliased', $node->class->toString()); + } + + #[Test] + public function theNullResultKeepsTheOriginalName(): void + { + $expected = Name::createFromString('Example'); + $node = new NamedTypeNode($expected); + + Traverser::new([new TypeMapVisitor(static fn(Name $name): ?Name => null)]) + ->traverse([$node]); + + self::assertSame($expected, $node->name); + } + + #[Test] + public function theTransformationIsAppliedToEveryNestedType(): void + { + $node = new UnionTypeNode([ + new NamedTypeNode(Name::createFromString('A')), + new NamedTypeNode(Name::createFromString('B')) + ]); + + Traverser::new([$this->alias()])->traverse([$node]); + + self::assertSame( + ['Aliased', 'Aliased'], + \array_map( + static fn(NamedTypeNode $type): string => $type->name->toString(), + $node->statements, + ), + ); + } + + #[Test] + public function theTransformationReceivesTheOriginalName(): void + { + $received = []; + + $visitor = new TypeMapVisitor(static function (Name $name) use (&$received): Name { + $received[] = $name->toString(); + + return $name; + }); + + Traverser::new([$visitor])->traverse([ + new NamedTypeNode(Name::createFromString('Foo\\Bar')), + ]); + + self::assertSame(['Foo\\Bar'], $received); + } + + #[Test] + public function theTransformationIsNotAppliedToAnArbitraryName(): void + { + $received = []; + + $visitor = new TypeMapVisitor(static function (Name $name) use (&$received): Name { + $received[] = $name->toString(); + + return $name; + }); + + Traverser::new([$visitor])->traverse([Name::createFromString('Example')]); + + self::assertSame([], $received); + } + + #[Test] + public function theTransformedNameIsVisitedInsteadOfTheOriginalOne(): void + { + $node = new NamedTypeNode(Name::createFromString('Example')); + $expected = Name::createFromString('Aliased'); + + Traverser::new([new TypeMapVisitor(static fn(Name $name): Name => $expected)]) + ->traverse([$node]); + + self::assertSame($expected, $node->name); + } +} diff --git a/libs/parser/tests/TypeParserFeaturesTest.php b/libs/parser/tests/TypeParserFeaturesTest.php new file mode 100644 index 0000000..11e6c24 --- /dev/null +++ b/libs/parser/tests/TypeParserFeaturesTest.php @@ -0,0 +1,186 @@ + + */ + public static function featureDataProvider(): iterable + { + yield 'conditions' => [ + 'conditions', + static fn(TypeParserFeatures $features): bool => $features->conditions, + TypeParserFeatures::CONDITIONAL_FEATURES_DEFAULT_VALUE, + ]; + + yield 'shapes' => [ + 'shapes', + static fn(TypeParserFeatures $features): bool => $features->shapes, + TypeParserFeatures::SHAPES_FEATURES_DEFAULT_VALUE, + ]; + + yield 'callables' => [ + 'callables', + static fn(TypeParserFeatures $features): bool => $features->callables, + TypeParserFeatures::CALLABLES_FEATURES_DEFAULT_VALUE, + ]; + + yield 'literals' => [ + 'literals', + static fn(TypeParserFeatures $features): bool => $features->literals, + TypeParserFeatures::LITERALS_FEATURES_DEFAULT_VALUE, + ]; + + yield 'generics' => [ + 'generics', + static fn(TypeParserFeatures $features): bool => $features->generics, + TypeParserFeatures::GENERICS_FEATURES_DEFAULT_VALUE, + ]; + + yield 'unions' => [ + 'unions', + static fn(TypeParserFeatures $features): bool => $features->unions, + TypeParserFeatures::UNION_FEATURES_DEFAULT_VALUE, + ]; + + yield 'intersections' => [ + 'intersections', + static fn(TypeParserFeatures $features): bool => $features->intersections, + TypeParserFeatures::INTERSECTION_FEATURES_DEFAULT_VALUE, + ]; + + yield 'lists' => [ + 'lists', + static fn(TypeParserFeatures $features): bool => $features->lists, + TypeParserFeatures::LIST_FEATURES_DEFAULT_VALUE, + ]; + + yield 'offsets' => [ + 'offsets', + static fn(TypeParserFeatures $features): bool => $features->offsets, + TypeParserFeatures::OFFSETS_FEATURES_DEFAULT_VALUE, + ]; + + yield 'hints' => [ + 'hints', + static fn(TypeParserFeatures $features): bool => $features->hints, + TypeParserFeatures::HINTS_FEATURES_DEFAULT_VALUE, + ]; + } + + /** + * @param non-empty-string $feature + * @param \Closure(TypeParserFeatures):bool $read + */ + #[DataProvider('featureDataProvider')] + #[Test] + public function eachFeatureIsEnabledByDefault(string $feature, \Closure $read, bool $default): void + { + self::assertTrue($default); + self::assertSame($default, $read(new TypeParserFeatures())); + } + + /** + * @param non-empty-string $feature + * @param \Closure(TypeParserFeatures):bool $read + */ + #[DataProvider('featureDataProvider')] + #[Test] + public function eachFeatureCanBeDisabledUsingTheConstructor(string $feature, \Closure $read, bool $default): void + { + self::assertFalse($read(new TypeParserFeatures(...[$feature => false]))); + } + + /** + * @param non-empty-string $feature + * @param \Closure(TypeParserFeatures):bool $read + */ + #[DataProvider('featureDataProvider')] + #[Test] + public function eachFeatureCanBeDisabledUsingTheWithMethod(string $feature, \Closure $read, bool $default): void + { + self::assertFalse($read((new TypeParserFeatures())->with(...[$feature => false]))); + } + + /** + * @param non-empty-string $feature + * @param \Closure(TypeParserFeatures):bool $read + */ + #[DataProvider('featureDataProvider')] + #[Test] + public function eachFeatureCanBeEnabledUsingTheWithMethod(string $feature, \Closure $read, bool $default): void + { + $features = new TypeParserFeatures(...[$feature => false]); + + self::assertTrue($read($features->with(...[$feature => true]))); + } + + #[Test] + public function withReturnsANewInstance(): void + { + $features = new TypeParserFeatures(); + + self::assertNotSame($features, $features->with(shapes: false)); + } + + #[Test] + public function withDoesNotModifyTheOriginalInstance(): void + { + $features = new TypeParserFeatures(); + $features->with(shapes: false); + + self::assertTrue($features->shapes); + } + + #[Test] + public function withKeepsTheNonSpecifiedFeatures(): void + { + $features = (new TypeParserFeatures(literals: false)) + ->with(shapes: false); + + self::assertFalse($features->literals); + self::assertFalse($features->shapes); + self::assertTrue($features->generics); + } + + #[Test] + public function withCanOverrideSeveralFeaturesAtOnce(): void + { + $features = (new TypeParserFeatures())->with( + unions: false, + intersections: false, + ); + + self::assertFalse($features->unions); + self::assertFalse($features->intersections); + } + + #[Test] + public function withoutArgumentsKeepsEveryFeature(): void + { + $features = new TypeParserFeatures(callables: false, offsets: false); + + self::assertEquals($features, $features->with()); + } + + #[Test] + public function withRejectsAnUnknownFeature(): void + { + $this->expectException(\Error::class); + + /** @phpstan-ignore-next-line : An unknown feature name is expected here */ + (new TypeParserFeatures())->with(unknownFeature: false); + } +} diff --git a/libs/parser/tests/TypeParserTest.php b/libs/parser/tests/TypeParserTest.php new file mode 100644 index 0000000..7a7d67c --- /dev/null +++ b/libs/parser/tests/TypeParserTest.php @@ -0,0 +1,151 @@ +features); + } + + #[Test] + public function theFeaturesArePassedThroughTheConstructor(): void + { + $features = new TypeParserFeatures(shapes: false); + + self::assertSame($features, (new TypeParser($features))->features); + } + + #[Test] + public function withFeaturesReturnsANewInstance(): void + { + $parser = new TypeParser(); + + self::assertNotSame($parser, $parser->withFeatures(shapes: false)); + } + + #[Test] + public function withFeaturesDoesNotModifyTheOriginalParser(): void + { + $parser = new TypeParser(); + $parser->withFeatures(shapes: false); + + self::assertTrue($parser->features->shapes); + } + + #[Test] + public function withFeaturesOverridesTheGivenFeature(): void + { + $parser = (new TypeParser())->withFeatures(shapes: false); + + self::assertFalse($parser->features->shapes); + } + + #[Test] + public function withFeaturesKeepsTheNonSpecifiedFeatures(): void + { + $parser = (new TypeParser(new TypeParserFeatures(literals: false))) + ->withFeatures(shapes: false); + + self::assertFalse($parser->features->literals); + self::assertTrue($parser->features->generics); + } + + #[Test] + public function withFeaturesAffectsTheParsingBehaviour(): void + { + $parser = (new TypeParser())->withFeatures(shapes: false); + + $this->expectException(ParserException::class); + + $parser->parse('array{a: int}'); + } + + #[Test] + public function theOriginalParserKeepsItsParsingBehaviour(): void + { + $parser = new TypeParser(); + $parser->withFeatures(shapes: false); + + self::assertInstanceOf(NamedTypeNode::class, $parser->parse('array{a: int}')); + } + + #[Test] + public function theSameParserCanBeUsedSeveralTimes(): void + { + $parser = new TypeParser(); + + self::assertEquals($parser->parse('int'), $parser->parse('int')); + } + + #[Test] + public function theSourceFactoryCanBeOverridden(): void + { + $parser = new TypeParser(sources: SourceFactory::createDefault()); + + self::assertInstanceOf(NamedTypeNode::class, $parser->parse('int')); + } + + #[Test] + public function theSourceFactoryIsInheritedByTheNewInstance(): void + { + $parser = (new TypeParser(sources: SourceFactory::createDefault())) + ->withFeatures(shapes: false); + + self::assertInstanceOf(NamedTypeNode::class, $parser->parse('int')); + } + + #[Test] + public function aPartialReadingOfAWholeSourceIsASuccessfulOne(): void + { + $result = (new TypeParser())->partial('int'); + + self::assertInstanceOf(SuccessfulParsedResult::class, $result); + self::assertNotInstanceOf(PartialParsedResult::class, $result); + self::assertInstanceOf(NamedTypeNode::class, $result->type); + self::assertSame('int', $result->type->name->toString()); + } + + #[Test] + public function theOffsetPointsToTheUnparsedTail(): void + { + $source = 'array{ field: result } This is an example'; + $result = (new TypeParser())->partial($source); + + self::assertInstanceOf(PartialParsedResult::class, $result); + self::assertSame('This is an example', \substr($source, $result->offset)); + } + + #[Test] + public function aPartialReadingOfNoTypeAtAllIsAFailure(): void + { + $result = (new TypeParser())->partial('|int'); + + self::assertInstanceOf(FailureParsedResult::class, $result); + } + + #[Test] + public function aPartialResultCanBeCreatedManually(): void + { + $type = new NamedTypeNode(Name::createFromString('int')); + $result = new PartialParsedResult($type, 42); + + self::assertSame($type, $result->type); + self::assertSame(42, $result->offset); + } +} diff --git a/libs/parser/tests/TypeResolver/NamespaceFinderTest.php b/libs/parser/tests/TypeResolver/NamespaceFinderTest.php new file mode 100644 index 0000000..202eca9 --- /dev/null +++ b/libs/parser/tests/TypeResolver/NamespaceFinderTest.php @@ -0,0 +1,154 @@ + + */ + private function rewind(string $namespace, string $source): \Iterator + { + $tokens = new \ArrayIterator(\PhpToken::tokenize("rewind($namespace, $tokens); + } + + /** + * Returns the text of the first meaningful token the stream stopped at. + */ + private function stoppedAt(string $namespace, string $source): ?string + { + $tokens = $this->rewind($namespace, $source); + + while ($tokens->valid()) { + $current = $tokens->current(); + + if ($current->id !== \T_WHITESPACE) { + return $current->text; + } + + $tokens->next(); + } + + return null; + } + + /** + * Returns every qualified name available after the stream position. + * + * @return list + */ + private function qualifiedNamesAfter(string $namespace, string $source): array + { + $tokens = $this->rewind($namespace, $source); + $result = []; + + while ($tokens->valid()) { + $current = $tokens->current(); + + if ($current->id === \T_NAME_QUALIFIED) { + $result[] = $current->text; + } + + $tokens->next(); + } + + return $result; + } + + #[Test] + public function theStreamIsMovedAfterTheMatchingNamespace(): void + { + self::assertSame('use', $this->stoppedAt('App', <<<'PHP' + namespace App; + + use Some\Any; + PHP)); + } + + #[Test] + public function theNestedNamespaceNameIsMatched(): void + { + self::assertSame('use', $this->stoppedAt('App\\Domain', <<<'PHP' + namespace App\Domain; + + use Some\Any; + PHP)); + } + + #[Test] + public function theNonMatchingNamespaceExhaustsTheStream(): void + { + self::assertNull($this->stoppedAt('Other', <<<'PHP' + namespace App; + + use Some\Any; + PHP)); + } + + #[Test] + public function theRequestedNamespaceIsFoundAmongSeveralOnes(): void + { + self::assertSame(['Second\\Import'], $this->qualifiedNamesAfter('Second', <<<'PHP' + namespace First; + + use First\Import; + + namespace Second; + + use Second\Import; + PHP)); + } + + #[Test] + public function theBracedNamespaceNameIsTerminatedByTheOpeningBrace(): void + { + self::assertSame(['Some\\Any'], $this->qualifiedNamesAfter('App', <<<'PHP' + namespace App { + use Some\Any; + } + PHP)); + } + + #[Test] + public function theGlobalNamespaceIsMatchedByAnEmptyName(): void + { + self::assertSame(['Some\\Any'], $this->qualifiedNamesAfter('', <<<'PHP' + namespace { + use Some\Any; + } + PHP)); + } + + /** + * A source without any namespace declaration belongs to the global one, so + * its import section starts at the very first "use" statement. + */ + #[Test] + public function theFirstImportIsFoundInASourceWithoutNamespaces(): void + { + self::assertSame('use', $this->stoppedAt('', <<<'PHP' + use Some\Any; + PHP)); + } + + #[Test] + public function theSourceWithoutImportsExhaustsTheStream(): void + { + self::assertNull($this->stoppedAt('Other', <<<'PHP' + final class Example {} + PHP)); + } +} diff --git a/libs/parser/tests/TypeResolver/PhpUseStatementsReaderTest.php b/libs/parser/tests/TypeResolver/PhpUseStatementsReaderTest.php index 187bdfa..f463811 100644 --- a/libs/parser/tests/TypeResolver/PhpUseStatementsReaderTest.php +++ b/libs/parser/tests/TypeResolver/PhpUseStatementsReaderTest.php @@ -5,6 +5,7 @@ namespace TypeLang\Parser\Tests\TypeResolver; use TypeLang\Parser\Tests\TypeResolver\Stub\ClassWithGroupUsesStub; +use TypeLang\Parser\Tests\TypeResolver\Stub\ClassWithMethodStub; use TypeLang\Parser\Tests\TypeResolver\Stub\ClosureUseStub; use TypeLang\Parser\Tests\TypeResolver\Stub\CommentsAroundUsesStub; use TypeLang\Parser\Tests\TypeResolver\Stub\FunctionAndConstUseStub; @@ -20,9 +21,7 @@ final class PhpUseStatementsReaderTest extends TypeResolverTestCase { - private PhpUseStatementsReader $reader { - get => $this->reader ??= new PhpUseStatementsReader(); - } + private ?PhpUseStatementsReader $reader = null; /** * @param class-string $class @@ -31,7 +30,25 @@ final class PhpUseStatementsReaderTest extends TypeResolverTestCase */ private function read(string $class): array { - return $this->reader->getClassUseStatements(new \ReflectionClass($class)); + return ($this->reader ??= new PhpUseStatementsReader())->getClassUseStatements(new \ReflectionClass($class)); + } + + /** + * @return array + */ + private function readFunction(\ReflectionFunctionAbstract $function): array + { + return ($this->reader ??= new PhpUseStatementsReader())->getFunctionUseStatements($function); + } + + /** + * @throws \ReflectionException + */ + private function functionStub(): \ReflectionFunction + { + require_once __DIR__ . '/Stub/functions.php'; + + return new \ReflectionFunction(__NAMESPACE__ . '\Stub\exampleFunctionStub'); } public function testReadsPlainAndAliasedImports(): void @@ -183,4 +200,33 @@ public function testReadsMultipleImportsInOneStatement(): void 'Some\C', ], $this->read(MultipleImportsPerStatementStub::class)); } + + public function testReadsImportsOfFunction(): void + { + self::assertSame([ + // use Some\Any; + 'Some\Any', + // use Some\Any\Test as Example; + 'Example' => 'Some\Any\Test', + // use function Some\helper; + 'Some\helper', + ], $this->readFunction($this->functionStub())); + } + + /** + * A method is not a namespaced symbol on its own, so its imports have to + * be read from the declaring class instead. + */ + public function testReturnsEmptyForMethod(): void + { + self::assertSame( + [], + $this->readFunction(new \ReflectionMethod(ClassWithMethodStub::class, 'example')), + ); + } + + public function testReturnsEmptyForInternalFunction(): void + { + self::assertSame([], $this->readFunction(new \ReflectionFunction('strlen'))); + } } diff --git a/libs/parser/tests/TypeResolver/ReflectionSourcePrefixReaderTest.php b/libs/parser/tests/TypeResolver/ReflectionSourcePrefixReaderTest.php new file mode 100644 index 0000000..bb0782f --- /dev/null +++ b/libs/parser/tests/TypeResolver/ReflectionSourcePrefixReaderTest.php @@ -0,0 +1,102 @@ +reader = new ReflectionSourcePrefixReader(); + } + + /** + * @throws \ReflectionException + */ + private function functionStub(): \ReflectionFunction + { + require_once __DIR__ . '/Stub/functions.php'; + + return new \ReflectionFunction(__NAMESPACE__ . '\Stub\exampleFunctionStub'); + } + + #[Test] + public function theClassHeaderContainsTheImports(): void + { + $header = $this->reader->readClassHeader(new \ReflectionClass(SimpleClassStub::class)); + + self::assertStringContainsString('use Some\Any;', $header); + self::assertStringContainsString('use Some\Any\Test as Example;', $header); + } + + #[Test] + public function theClassHeaderDoesNotContainTheDeclarationItself(): void + { + $header = $this->reader->readClassHeader(new \ReflectionClass(SimpleClassStub::class)); + + self::assertStringNotContainsString('final class SimpleClassStub', $header); + } + + #[Test] + public function theClassHeaderIsEmptyForAnInternalClass(): void + { + self::assertSame('', $this->reader->readClassHeader(new \ReflectionClass(\stdClass::class))); + } + + #[Test] + public function theFunctionHeaderContainsTheImports(): void + { + $header = $this->reader->readFunctionHeader($this->functionStub()); + + self::assertStringContainsString('use Some\Any;', $header); + self::assertStringContainsString('use function Some\helper;', $header); + } + + #[Test] + public function theFunctionHeaderDoesNotContainTheDeclarationItself(): void + { + $header = $this->reader->readFunctionHeader($this->functionStub()); + + self::assertStringNotContainsString('function exampleFunctionStub', $header); + } + + #[Test] + public function theFunctionHeaderIsEmptyForAnInternalFunction(): void + { + self::assertSame('', $this->reader->readFunctionHeader(new \ReflectionFunction('strlen'))); + } + + #[Test] + public function theMethodHeaderIsReadFromItsOwnFile(): void + { + $header = $this->reader->readFunctionHeader( + new \ReflectionMethod(ClassWithMethodStub::class, 'example'), + ); + + self::assertStringContainsString('use Some\Method\Any;', $header); + } + + #[Test] + public function theHeaderStartsAtTheBeginningOfTheFile(): void + { + $header = $this->reader->readClassHeader(new \ReflectionClass(SimpleClassStub::class)); + + self::assertStringStartsWith('resolve($this->parse($code)); + + self::assertInstanceOf(NamedTypeNode::class, $node); + + return $node->name->toString(); + } + + /** + * @throws \ReflectionException + */ + private function functionStub(): \ReflectionFunction + { + require_once __DIR__ . '/Stub/functions.php'; + + return new \ReflectionFunction(__NAMESPACE__ . '\\Stub\\exampleFunctionStub'); + } + + #[Test] + public function theNonAliasedImportOfAClassIsResolved(): void + { + $resolver = (new TypeResolver()) + ->withTypeImportsFromClass(new \ReflectionClass(SimpleClassStub::class)); + + self::assertSame('Some\\Any', $this->resolveName($resolver, 'Any')); + } + + #[Test] + public function theAliasedImportOfAClassIsResolved(): void + { + $resolver = (new TypeResolver()) + ->withTypeImportsFromClass(new \ReflectionClass(SimpleClassStub::class)); + + self::assertSame( + 'Some\\Any\\Test\\Nested', + $this->resolveName($resolver, 'Example\\Nested'), + ); + } + + #[Test] + public function withTypeImportsFromClassReturnsANewInstance(): void + { + $resolver = new TypeResolver(); + + self::assertNotSame( + $resolver, + $resolver->withTypeImportsFromClass(new \ReflectionClass(SimpleClassStub::class)), + ); + } + + #[Test] + public function withTypeImportsFromClassDoesNotModifyTheOriginalResolver(): void + { + $resolver = new TypeResolver(); + $resolver->withTypeImportsFromClass(new \ReflectionClass(SimpleClassStub::class)); + + self::assertSame('Any', $this->resolveName($resolver, 'Any')); + } + + #[Test] + public function theImportsOfAClassAreMergedWithTheExistingOnes(): void + { + $resolver = (new TypeResolver()) + ->withTypeImport('App\\Node') + ->withTypeImportsFromClass(new \ReflectionClass(SimpleClassStub::class)); + + self::assertSame('App\\Node', $this->resolveName($resolver, 'Node')); + self::assertSame('Some\\Any', $this->resolveName($resolver, 'Any')); + } + + #[Test] + public function theClassWithoutImportsChangesNothing(): void + { + $resolver = (new TypeResolver()) + ->withTypeImportsFromClass(new \ReflectionClass(NoImportsStub::class)); + + self::assertSame('Any', $this->resolveName($resolver, 'Any')); + } + + #[Test] + public function theInternalClassChangesNothing(): void + { + $resolver = (new TypeResolver()) + ->withTypeImportsFromClass(new \ReflectionClass(\stdClass::class)); + + self::assertSame('Any', $this->resolveName($resolver, 'Any')); + } + + #[Test] + public function theNonAliasedImportOfAFunctionIsResolved(): void + { + $resolver = (new TypeResolver()) + ->withTypeImportsFromFunction($this->functionStub()); + + self::assertSame('Some\\Any', $this->resolveName($resolver, 'Any')); + } + + #[Test] + public function theAliasedImportOfAFunctionIsResolved(): void + { + $resolver = (new TypeResolver()) + ->withTypeImportsFromFunction($this->functionStub()); + + self::assertSame( + 'Some\\Any\\Test\\Nested', + $this->resolveName($resolver, 'Example\\Nested'), + ); + } + + #[Test] + public function withTypeImportsFromFunctionReturnsANewInstance(): void + { + $resolver = new TypeResolver(); + + self::assertNotSame( + $resolver, + $resolver->withTypeImportsFromFunction($this->functionStub()), + ); + } + + #[Test] + public function withTypeImportsFromFunctionDoesNotModifyTheOriginalResolver(): void + { + $resolver = new TypeResolver(); + $resolver->withTypeImportsFromFunction($this->functionStub()); + + self::assertSame('Any', $this->resolveName($resolver, 'Any')); + } + + #[Test] + public function theImportsOfAMethodAreReadFromItsDeclaringClass(): void + { + $resolver = (new TypeResolver()) + ->withTypeImportsFromFunction( + new \ReflectionMethod(ClassWithMethodStub::class, 'example'), + ); + + self::assertSame('Some\\Method\\Any', $this->resolveName($resolver, 'Any')); + self::assertSame( + 'Some\\Method\\Any\\Test', + $this->resolveName($resolver, 'Example'), + ); + } + + #[Test] + public function theInternalFunctionChangesNothing(): void + { + $resolver = (new TypeResolver()) + ->withTypeImportsFromFunction(new \ReflectionFunction('strlen')); + + self::assertSame('Any', $this->resolveName($resolver, 'Any')); + } + + #[Test] + public function theClosureIsReadWithTheImportsOfItsFile(): void + { + $resolver = (new TypeResolver()) + ->withTypeImportsFromFunction( + new \ReflectionFunction(static fn(): int => 42), + ); + + self::assertSame( + 'TypeLang\\Parser\\TypeResolver', + $this->resolveName($resolver, 'TypeResolver'), + ); + } +} diff --git a/libs/parser/tests/TypeResolver/TypeResolverTest.php b/libs/parser/tests/TypeResolver/TypeResolverTest.php index 24c84ec..207607a 100644 --- a/libs/parser/tests/TypeResolver/TypeResolverTest.php +++ b/libs/parser/tests/TypeResolver/TypeResolverTest.php @@ -33,7 +33,7 @@ private function resolveName(TypeResolver $resolver, string $code): string public function testResolvesImportedName(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('TypeLang\Parser\Node'); self::assertSame( @@ -44,7 +44,7 @@ public function testResolvesImportedName(): void public function testResolvesImportedNamePrefix(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('TypeLang\Parser\Node'); self::assertSame( @@ -55,7 +55,7 @@ public function testResolvesImportedNamePrefix(): void public function testResolvesAliasedName(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImportAs('TypeLang\Parser\Exception', 'Error'); self::assertSame( @@ -66,7 +66,7 @@ public function testResolvesAliasedName(): void public function testResolvesAliasItself(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImportAs('TypeLang\Parser\Exception', 'Error'); self::assertSame( @@ -93,7 +93,7 @@ public static function caseInsensitiveDataProvider(): iterable #[DataProvider('caseInsensitiveDataProvider')] public function testMatchesFirstSegmentCaseInsensitively(string $code, string $expected): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('TypeLang\Parser\Node'); self::assertSame($expected, $this->resolveName($resolver, $code)); @@ -101,7 +101,7 @@ public function testMatchesFirstSegmentCaseInsensitively(string $code, string $e public function testLeavesUnknownNameUnchanged(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('TypeLang\Parser\Node'); self::assertSame('Unknown', $this->resolveName($resolver, 'Unknown')); @@ -117,7 +117,7 @@ public function testEmptyResolverLeavesNameUnchanged(): void public function testBuiltinTypeIsLeftUnchanged(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('TypeLang\Parser\Node'); self::assertSame('int', $this->resolveName($resolver, 'int')); @@ -125,7 +125,7 @@ public function testBuiltinTypeIsLeftUnchanged(): void public function testLastImportWinsForDuplicateLastSegment(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('First\Node') ->withTypeImport('Second\Node'); @@ -134,7 +134,7 @@ public function testLastImportWinsForDuplicateLastSegment(): void public function testResolvesNamesInsideUnion(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('TypeLang\Parser\Node') ->withTypeImportAs('TypeLang\Parser\Exception', 'Error'); @@ -142,67 +142,49 @@ public function testResolvesNamesInsideUnion(): void UnionTypeNode NamedTypeNode Name(TypeLang\Parser\Node) - Identifier(TypeLang) - Identifier(Parser) - Identifier(Node) NamedTypeNode Name(TypeLang\Parser\Exception) - Identifier(TypeLang) - Identifier(Parser) - Identifier(Exception) AST, $this->print($resolver->resolve($this->parse('Node|Error')))); } public function testResolvesNamesInsideShape(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('TypeLang\Parser\Node') ->withTypeImportAs('TypeLang\Parser\Exception', 'Error'); self::assertSame(<<<'AST' NamedTypeNode Name(array) - Identifier(array) - Shape\FieldsListNode(sealed) - Shape\ImplicitFieldNode(required) + Shape\FieldsListNode(isSealed=true) + Shape\ImplicitFieldNode(isOptional=false) NamedTypeNode Name(TypeLang\Parser\Node) - Identifier(TypeLang) - Identifier(Parser) - Identifier(Node) - Shape\ImplicitFieldNode(required) + Shape\ImplicitFieldNode(isOptional=false) NamedTypeNode Name(TypeLang\Parser\Exception\SemanticException) - Identifier(TypeLang) - Identifier(Parser) - Identifier(Exception) - Identifier(SemanticException) AST, $this->print($resolver->resolve($this->parse('array{Node, Error\SemanticException}')))); } public function testResolvesNamesInsideGenericArguments(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('Vendor\Collection') ->withTypeImport('Vendor\Node'); self::assertSame(<<<'AST' NamedTypeNode Name(Vendor\Collection) - Identifier(Vendor) - Identifier(Collection) Template\TemplateArgumentListNode Template\TemplateArgumentNode NamedTypeNode Name(Vendor\Node) - Identifier(Vendor) - Identifier(Node) AST, $this->print($resolver->resolve($this->parse('Collection')))); } public function testResolveMutatesInPlaceAndReturnsSameInstance(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('TypeLang\Parser\Node'); $source = $this->parse('Node'); @@ -262,7 +244,7 @@ public static function aliasCaseDataProvider(): iterable #[DataProvider('aliasCaseDataProvider')] public function testMatchesAliasCaseInsensitively(string $code, string $expected): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImportAs('TypeLang\Parser\Exception', 'Error'); self::assertSame($expected, $this->resolveName($resolver, $code)); @@ -270,58 +252,50 @@ public function testMatchesAliasCaseInsensitively(string $code, string $expected public function testResolvesNamesInsideCallableType(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('App\Node') ->withTypeImportAs('App\Err\Exception', 'Error'); self::assertSame(<<<'AST' CallableTypeNode Name(callable) - Identifier(callable) Callable\CallableParameterListNode - Callable\CallableParameterNode(simple) + Callable\CallableParameterNode(isOutput=false, isVariadic=false, isOptional=false) NamedTypeNode Name(App\Node) - Identifier(App) - Identifier(Node) NamedTypeNode Name(App\Err\Exception) - Identifier(App) - Identifier(Err) - Identifier(Exception) AST, $this->print($resolver->resolve($this->parse('callable(Node): Error')))); } public function testResolvesClassInClassConstant(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('App\Node'); self::assertSame(<<<'AST' ClassConstNode Name(App\Node) - Identifier(App) - Identifier(Node) Identifier(FOO) AST, $this->print($resolver->resolve($this->parse('Node::FOO')))); } public function testResolvesClassInClassConstantMask(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('App\Node'); self::assertSame(<<<'AST' ClassConstMaskNode Name(App\Node) - Identifier(App) - Identifier(Node) + MaskNode(*) + WildcardNode(*) AST, $this->print($resolver->resolve($this->parse('Node::*')))); } public function testResolvesNamesInsideIntersection(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('App\Node') ->withTypeImportAs('App\Err\Exception', 'Error'); @@ -329,19 +303,14 @@ public function testResolvesNamesInsideIntersection(): void IntersectionTypeNode NamedTypeNode Name(App\Node) - Identifier(App) - Identifier(Node) NamedTypeNode Name(App\Err\Exception) - Identifier(App) - Identifier(Err) - Identifier(Exception) AST, $this->print($resolver->resolve($this->parse('Node&Error')))); } public function testResolvesNamesInsideConditional(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('App\Node') ->withTypeImportAs('App\Err\Exception', 'Error'); @@ -350,13 +319,8 @@ public function testResolvesNamesInsideConditional(): void Condition\EqualConditionNode NamedTypeNode Name(App\Node) - Identifier(App) - Identifier(Node) NamedTypeNode Name(App\Err\Exception) - Identifier(App) - Identifier(Err) - Identifier(Exception) Literal\BoolLiteralNode(true) Literal\BoolLiteralNode(false) AST, $this->print($resolver->resolve($this->parse('Node is Error ? true : false')))); @@ -368,7 +332,7 @@ public function testResolvesNamesInsideConditional(): void */ public function testDoesNotResolveNamespaceRelativePrefix(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('App\Node'); self::assertSame('namespace\Node', $this->resolveName($resolver, 'namespace\Node')); @@ -393,7 +357,7 @@ public static function nonMatchingNameDataProvider(): iterable #[DataProvider('nonMatchingNameDataProvider')] public function testDoesNotResolveNonMatchingName(string $code): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('App\Node') ->withTypeImportAs('App\Err\Exception', 'Error'); @@ -418,7 +382,7 @@ public static function literalTypeDataProvider(): iterable #[DataProvider('literalTypeDataProvider')] public function testLeavesLiteralTypesUntouched(string $code): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('App\Node'); $source = $this->parse($code); @@ -436,7 +400,7 @@ public function testLeavesLiteralTypesUntouched(string $code): void */ public function testLeavesUnrelatedCompoundTypeUntouched(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('App\Node'); $source = $this->parse('array|callable(bool): void'); @@ -453,7 +417,7 @@ public function testEmptyResolverIsANoOp(): void $source = $this->parse('array{Node, Error\Sub}'); $before = $this->print($source); - self::assertSame($before, $this->print(new TypeResolver()->resolve($source))); + self::assertSame($before, $this->print((new TypeResolver())->resolve($source))); } /** @@ -463,7 +427,7 @@ public function testEmptyResolverIsANoOp(): void */ public function testMalformedImportIsRejected(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('\\'); $this->expectException(\Throwable::class); @@ -492,7 +456,7 @@ public static function fullyQualifiedNameDataProvider(): iterable #[DataProvider('fullyQualifiedNameDataProvider')] public function testFullyQualifiedNamesIgnoreImports(string $code, string $expected): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('TypeLang\Parser\Node') ->withTypeImportAs('TypeLang\Parser\Exception', 'Error'); @@ -525,7 +489,7 @@ public static function reservedBuiltinTypeDataProvider(): iterable #[DataProvider('reservedBuiltinTypeDataProvider')] public function testReservedBuiltinTypeIsNeverRewrittenByImport(string $reserved): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport("Vendor\\{$reserved}"); self::assertSame($reserved, $this->resolveName($resolver, $reserved)); @@ -547,7 +511,7 @@ public static function reservedSpecialTypeDataProvider(): iterable #[DataProvider('reservedSpecialTypeDataProvider')] public function testReservedSpecialTypeIsNeverRewrittenByImport(string $reserved): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport("Vendor\\{$reserved}"); self::assertSame($reserved, $this->resolveName($resolver, $reserved)); @@ -571,7 +535,7 @@ public static function nonMatchingSegmentDataProvider(): iterable #[DataProvider('nonMatchingSegmentDataProvider')] public function testImportMatchesWholeSegmentOnly(string $code): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('A\Node'); self::assertSame($code, $this->resolveName($resolver, $code)); @@ -584,7 +548,7 @@ public function testImportMatchesWholeSegmentOnly(string $code): void */ public function testResolutionIsIdempotent(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImport('TypeLang\Parser\Node'); $node = $this->parse('Node\Foo'); @@ -602,7 +566,7 @@ public function testResolutionIsIdempotent(): void */ public function testAliasSubstitutionKeepsAllTrailingSegments(): void { - $resolver = new TypeResolver() + $resolver = (new TypeResolver()) ->withTypeImportAs('A\B\Exception', 'Error'); self::assertSame( diff --git a/libs/phpdoc/.github/workflows/tests.yml b/libs/phpdoc/.github/workflows/tests.yml index a7df15d..3abc5cb 100644 --- a/libs/phpdoc/.github/workflows/tests.yml +++ b/libs/phpdoc/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - php: [ '8.4', '8.5' ] + php: [ '8.1', '8.2', '8.3', '8.4', '8.5' ] os: [ ubuntu-latest, macos-latest, windows-latest ] stability: [ lowest, stable ] steps: diff --git a/libs/phpdoc/README.md b/libs/phpdoc/README.md index a7b74f4..b19cfa8 100644 --- a/libs/phpdoc/README.md +++ b/libs/phpdoc/README.md @@ -28,7 +28,7 @@ composer require type-lang/phpdoc ``` **Requirements:** -- PHP 8.4+ +- PHP 8.1+ ## Usage diff --git a/libs/phpdoc/composer.json b/libs/phpdoc/composer.json index 663f4f6..76c4fd4 100644 --- a/libs/phpdoc/composer.json +++ b/libs/phpdoc/composer.json @@ -9,7 +9,7 @@ "issues": "https://github.com/php-type-language/phpdoc/issues" }, "require": { - "php": "^8.4", + "php": "^8.1", "type-lang/parser": "^2.0" }, "autoload": { @@ -18,9 +18,8 @@ } }, "require-dev": { - "phpstan/phpdoc-parser": "^2.3", - "phpstan/phpstan": "^2.2.5", - "phpunit/phpunit": "^13.2.4" + "phpstan/phpstan": "^2.2", + "phpunit/phpunit": "^10.5|^11.0|^12.0|^13.0" }, "autoload-dev": { "psr-4": { diff --git a/libs/phpdoc/phpstan.neon b/libs/phpdoc/phpstan.neon index 018e46e..f2d4b90 100644 --- a/libs/phpdoc/phpstan.neon +++ b/libs/phpdoc/phpstan.neon @@ -1,5 +1,9 @@ parameters: level: max + phpVersion: + min: 80100 + max: 80599 + treatPhpDocTypesAsCertain: false fileExtensions: - php paths: diff --git a/libs/phpdoc/src/DocBlock/Combinator/AssertSubjectCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/AssertSubjectCombinator.php new file mode 100644 index 0000000..42b52cf --- /dev/null +++ b/libs/phpdoc/src/DocBlock/Combinator/AssertSubjectCombinator.php @@ -0,0 +1,83 @@ +items + * $this->getItems() + * self::$instances + * ``` + * + * @template-implements CombinatorInterface + */ +final class AssertSubjectCombinator implements CombinatorInterface +{ + public const NAME = 'AssertSubject'; + + public function __invoke(Cursor $cursor): CodeReference + { + $subject = $this->parse($cursor); + + // A subject is a single word: nothing but whitespace may follow it. + if ($subject === null || $cursor->readWord() !== '') { + throw new NoMatchException('Expected a variable, a property or a method'); + } + + return $subject; + } + + private function parse(Cursor $cursor): ?CodeReference + { + // "$value", "$this->items" and "$this->getItems()" + if ($cursor->readLiteral('$')) { + $variable = $cursor->readPhpIdentifier(); + + if ($variable === '') { + return null; + } + + if (!$cursor->readLiteral('->')) { + return new VariableReference($variable); + } + + $member = $cursor->readPhpIdentifier(); + + if ($member === '') { + return null; + } + + return $cursor->readLiteral('()') + ? new VariableMethodReference($variable, $member) + : new VariablePropertyReference($variable, $member); + } + + // "self::$instances" + $class = $cursor->readPhpQualifiedName(); + + if ($class === '' || !$cursor->readLiteral('::$')) { + return null; + } + + $property = $cursor->readPhpIdentifier(); + + return $property === '' ? null : new ClassPropertyReference($class, $property); + } +} diff --git a/libs/phpdoc/src/DocBlock/Combinator/AuthorNameCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/AuthorNameCombinator.php index 4d38ec9..c4784b6 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/AuthorNameCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/AuthorNameCombinator.php @@ -13,9 +13,9 @@ * * @template-implements CombinatorInterface */ -final readonly class AuthorNameCombinator implements CombinatorInterface +final class AuthorNameCombinator implements CombinatorInterface { - public const string NAME = 'AuthorName'; + public const NAME = 'AuthorName'; /** * @return non-empty-string diff --git a/libs/phpdoc/src/DocBlock/Combinator/CallableTypeCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/CallableTypeCombinator.php index 67e2938..43e0e86 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/CallableTypeCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/CallableTypeCombinator.php @@ -13,9 +13,9 @@ * Reads a type and accepts it only when it is a callable, so that a plain word * (which would parse as a type) is left for the following combinators. */ -final readonly class CallableTypeCombinator extends TypeCombinator +final class CallableTypeCombinator extends TypeCombinator { - public const string NAME = 'CallableType'; + public const NAME = 'CallableType'; #[\Override] public function __invoke(Cursor $cursor): TypeReference diff --git a/libs/phpdoc/src/DocBlock/Combinator/DescriptionCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/DescriptionCombinator.php index 0e90bd7..85e4513 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/DescriptionCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/DescriptionCombinator.php @@ -19,12 +19,12 @@ * * @template-implements CombinatorInterface */ -final readonly class DescriptionCombinator implements CombinatorInterface +final class DescriptionCombinator implements CombinatorInterface { - public const string NAME = 'Description'; + public const NAME = 'Description'; public function __construct( - private DescriptionParserInterface $descriptionParser, + private readonly DescriptionParserInterface $descriptionParser, ) {} public function __invoke(Cursor $cursor): DescriptionInterface diff --git a/libs/phpdoc/src/DocBlock/Combinator/EmailCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/EmailCombinator.php index f7dd923..5710218 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/EmailCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/EmailCombinator.php @@ -13,9 +13,9 @@ * * @template-implements CombinatorInterface */ -final readonly class EmailCombinator implements CombinatorInterface +final class EmailCombinator implements CombinatorInterface { - public const string NAME = 'Email'; + public const NAME = 'Email'; /** * @return non-empty-string diff --git a/libs/phpdoc/src/DocBlock/Combinator/FlowTypeCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/FlowTypeCombinator.php index ec98809..4583140 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/FlowTypeCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/FlowTypeCombinator.php @@ -14,9 +14,9 @@ * * @template-implements CombinatorInterface */ -final readonly class FlowTypeCombinator implements CombinatorInterface +final class FlowTypeCombinator implements CombinatorInterface { - public const string NAME = 'FlowType'; + public const NAME = 'FlowType'; public function __invoke(Cursor $cursor): FlowType { diff --git a/libs/phpdoc/src/DocBlock/Combinator/IntegerCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/IntegerCombinator.php index 72f8a89..8a3b7f9 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/IntegerCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/IntegerCombinator.php @@ -13,11 +13,11 @@ * * @template-implements CombinatorInterface> */ -final readonly class IntegerCombinator implements CombinatorInterface +final class IntegerCombinator implements CombinatorInterface { - public const string NAME = 'Integer'; + public const NAME = 'Integer'; - private const string DIGITS = '0123456789'; + private const DIGITS = '0123456789'; /** * @return int<0, max> diff --git a/libs/phpdoc/src/DocBlock/Combinator/IssueNameCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/IssueNameCombinator.php index 319a1bf..3221002 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/IssueNameCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/IssueNameCombinator.php @@ -14,11 +14,11 @@ * * @template-implements CombinatorInterface */ -final readonly class IssueNameCombinator implements CombinatorInterface +final class IssueNameCombinator implements CombinatorInterface { - public const string NAME = 'IssueName'; + public const NAME = 'IssueName'; - private const string CHARS = 'abcdefghijklmnopqrstuvwxyz' + private const CHARS = 'abcdefghijklmnopqrstuvwxyz' . 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' . '0123456789_.-'; diff --git a/libs/phpdoc/src/DocBlock/Combinator/NameCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/NameCombinator.php index 4111e7f..eadd1ca 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/NameCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/NameCombinator.php @@ -13,9 +13,9 @@ * * @template-implements CombinatorInterface */ -final readonly class NameCombinator implements CombinatorInterface +final class NameCombinator implements CombinatorInterface { - public const string NAME = 'Name'; + public const NAME = 'Name'; /** * @return non-empty-string diff --git a/libs/phpdoc/src/DocBlock/Combinator/ReferenceCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/ReferenceCombinator.php index 4b9bafa..1993635 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/ReferenceCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/ReferenceCombinator.php @@ -19,9 +19,9 @@ * Reads a reference to a code element: a class, a function, a class method, a * class constant, a class property or a variable. */ -final readonly class ReferenceCombinator implements CombinatorInterface +final class ReferenceCombinator implements CombinatorInterface { - public const string NAME = 'Reference'; + public const NAME = 'Reference'; public function __invoke(Cursor $cursor): CodeReference { diff --git a/libs/phpdoc/src/DocBlock/Combinator/TypeCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/TypeCombinator.php index 389a83a..86f8563 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/TypeCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/TypeCombinator.php @@ -4,6 +4,7 @@ namespace TypeLang\PhpDoc\DocBlock\Combinator; +use TypeLang\Parser\Partial\SuccessfulParsedResult; use TypeLang\Parser\TypeParserInterface; use TypeLang\PhpDoc\DocBlock\Reference\TypeReference; use TypeLang\PhpDoc\Parser\Grammar\CombinatorInterface; @@ -18,29 +19,30 @@ * * @template-implements CombinatorInterface */ -readonly class TypeCombinator implements CombinatorInterface +class TypeCombinator implements CombinatorInterface { - public const string NAME = 'Type'; + public const NAME = 'Type'; public function __construct( - private TypeParserInterface $typeParser + private readonly TypeParserInterface $typeParser ) {} public function __invoke(Cursor $cursor): TypeReference { - $start = $cursor->position; + $start = $cursor->getPosition(); $source = $cursor->readRemainder(); if ($source === '') { throw new NoMatchException('Expected a type'); } - // Tolerant parsing yields the type together with the offset of the next - // token after it, so the cursor is left at the start of the trailing - // text (e.g. a description) rather than at the end of the buffer. - $result = $this->typeParser->parseTolerant($source); + $result = $this->typeParser->partial($source); - $cursor->position = $start + $result->offset; + if (!$result instanceof SuccessfulParsedResult) { + throw new NoMatchException($result->message); + } + + $cursor->moveTo($start + $result->offset); // The tolerant offset also covers the whitespace up to the next token, // so the trailing run is trimmed off the preserved type text. diff --git a/libs/phpdoc/src/DocBlock/Combinator/UriCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/UriCombinator.php index 492ef98..719b13b 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/UriCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/UriCombinator.php @@ -15,9 +15,9 @@ * * @template-implements CombinatorInterface */ -final readonly class UriCombinator implements CombinatorInterface +final class UriCombinator implements CombinatorInterface { - public const string NAME = 'URI'; + public const NAME = 'URI'; public function __invoke(Cursor $cursor): UriReference { diff --git a/libs/phpdoc/src/DocBlock/Combinator/UrlCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/UrlCombinator.php index 40f66a4..32196ea 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/UrlCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/UrlCombinator.php @@ -17,9 +17,9 @@ * * @template-implements CombinatorInterface */ -final readonly class UrlCombinator implements CombinatorInterface +final class UrlCombinator implements CombinatorInterface { - public const string NAME = 'URL'; + public const NAME = 'URL'; public function __invoke(Cursor $cursor): UrlReference { diff --git a/libs/phpdoc/src/DocBlock/Combinator/VariableCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/VariableCombinator.php index 4aa5db5..213f788 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/VariableCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/VariableCombinator.php @@ -13,9 +13,9 @@ * * @template-implements CombinatorInterface */ -final readonly class VariableCombinator implements CombinatorInterface +final class VariableCombinator implements CombinatorInterface { - public const string NAME = 'Variable'; + public const NAME = 'Variable'; /** * @return non-empty-string diff --git a/libs/phpdoc/src/DocBlock/Combinator/VersionCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/VersionCombinator.php index 7c39d8d..106613a 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/VersionCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/VersionCombinator.php @@ -14,11 +14,11 @@ * * @template-implements CombinatorInterface */ -final readonly class VersionCombinator implements CombinatorInterface +final class VersionCombinator implements CombinatorInterface { - public const string NAME = 'Version'; + public const NAME = 'Version'; - private const string DIGITS = '0123456789'; + private const DIGITS = '0123456789'; /** * @return non-empty-string diff --git a/libs/phpdoc/src/DocBlock/Combinator/VisibilityCombinator.php b/libs/phpdoc/src/DocBlock/Combinator/VisibilityCombinator.php index 285e5dc..730eea3 100644 --- a/libs/phpdoc/src/DocBlock/Combinator/VisibilityCombinator.php +++ b/libs/phpdoc/src/DocBlock/Combinator/VisibilityCombinator.php @@ -15,9 +15,9 @@ * * @template-implements CombinatorInterface */ -final readonly class VisibilityCombinator implements CombinatorInterface +final class VisibilityCombinator implements CombinatorInterface { - public const string NAME = 'Visibility'; + public const NAME = 'Visibility'; public function __invoke(Cursor $cursor): Visibility { diff --git a/libs/phpdoc/src/DocBlock/Description/Description.php b/libs/phpdoc/src/DocBlock/Description/Description.php index 37b280a..14e642d 100644 --- a/libs/phpdoc/src/DocBlock/Description/Description.php +++ b/libs/phpdoc/src/DocBlock/Description/Description.php @@ -4,10 +4,10 @@ namespace TypeLang\PhpDoc\DocBlock\Description; -final readonly class Description implements DescriptionInterface +final class Description implements DescriptionInterface { public function __construct( - public string $value = '', + public readonly string $value = '', ) {} public static function createIfNotEmpty(string $value): ?self diff --git a/libs/phpdoc/src/DocBlock/Description/TaggedDescription.php b/libs/phpdoc/src/DocBlock/Description/TaggedDescription.php index 1a19392..689f4c7 100644 --- a/libs/phpdoc/src/DocBlock/Description/TaggedDescription.php +++ b/libs/phpdoc/src/DocBlock/Description/TaggedDescription.php @@ -30,20 +30,22 @@ final class TaggedDescription implements public readonly array $components; /** - * Gets a list of all tags within a description + * A list of all tags within a description * * @var list */ - public array $tags { - get => $this->tags ??= $this->only(TagInterface::class); - } + public readonly array $tags; /** * @param iterable $components */ public function __construct(iterable $components = []) { - $this->components = \iterator_to_array($components, false); + $this->components = \is_array($components) + ? \array_values($components) + : \iterator_to_array($components, false); + + $this->tags = $this->only(TagInterface::class); } /** diff --git a/libs/phpdoc/src/DocBlock/DocBlock.php b/libs/phpdoc/src/DocBlock/DocBlock.php index b5fbd1d..d1136a8 100644 --- a/libs/phpdoc/src/DocBlock/DocBlock.php +++ b/libs/phpdoc/src/DocBlock/DocBlock.php @@ -33,7 +33,7 @@ * @template-implements \ArrayAccess * @template-implements \IteratorAggregate */ -final readonly class DocBlock implements +final class DocBlock implements ComponentInterface, \IteratorAggregate, \ArrayAccess, @@ -58,7 +58,7 @@ * * @var list */ - public array $tags; + public readonly array $tags; /** * @param iterable $tags list of all tags contained @@ -86,10 +86,12 @@ public function __construct( * echo $block->description; // "Sends a notification..." * ``` */ - public ?DescriptionInterface $description = null, + public readonly ?DescriptionInterface $description = null, iterable $tags = [], ) { - $this->tags = \iterator_to_array($tags, false); + $this->tags = \is_array($tags) + ? \array_values($tags) + : \iterator_to_array($tags, false); } public function offsetExists(mixed $offset): bool diff --git a/libs/phpdoc/src/DocBlock/Reference/ClassConstantReference.php b/libs/phpdoc/src/DocBlock/Reference/ClassConstantReference.php index d833bcd..ad67490 100644 --- a/libs/phpdoc/src/DocBlock/Reference/ClassConstantReference.php +++ b/libs/phpdoc/src/DocBlock/Reference/ClassConstantReference.php @@ -7,7 +7,7 @@ /** * A reference to a constant of a class. */ -final readonly class ClassConstantReference extends CodeReference +final class ClassConstantReference extends CodeReference { public function __construct( /** @@ -15,13 +15,13 @@ public function __construct( * * @var non-empty-string */ - public string $class, + public readonly string $class, /** * The name of the referenced constant. * * @var non-empty-string */ - public string $name, + public readonly string $name, ) { parent::__construct(); } diff --git a/libs/phpdoc/src/DocBlock/Reference/ClassMethodReference.php b/libs/phpdoc/src/DocBlock/Reference/ClassMethodReference.php index 30ba4b4..ac84c11 100644 --- a/libs/phpdoc/src/DocBlock/Reference/ClassMethodReference.php +++ b/libs/phpdoc/src/DocBlock/Reference/ClassMethodReference.php @@ -7,7 +7,7 @@ /** * A reference to a method of a class. */ -final readonly class ClassMethodReference extends CodeReference +final class ClassMethodReference extends CodeReference { public function __construct( /** @@ -15,13 +15,13 @@ public function __construct( * * @var non-empty-string */ - public string $class, + public readonly string $class, /** * The name of the referenced method. * * @var non-empty-string */ - public string $name, + public readonly string $name, ) { parent::__construct(); } diff --git a/libs/phpdoc/src/DocBlock/Reference/ClassPropertyReference.php b/libs/phpdoc/src/DocBlock/Reference/ClassPropertyReference.php index b11a0b4..d3a40b5 100644 --- a/libs/phpdoc/src/DocBlock/Reference/ClassPropertyReference.php +++ b/libs/phpdoc/src/DocBlock/Reference/ClassPropertyReference.php @@ -7,7 +7,7 @@ /** * A reference to a property of a class. */ -final readonly class ClassPropertyReference extends CodeReference +final class ClassPropertyReference extends CodeReference { public function __construct( /** @@ -15,13 +15,13 @@ public function __construct( * * @var non-empty-string */ - public string $class, + public readonly string $class, /** * The name of the referenced property, without the leading "$". * * @var non-empty-string */ - public string $name, + public readonly string $name, ) { parent::__construct(); } diff --git a/libs/phpdoc/src/DocBlock/Reference/CodeReference.php b/libs/phpdoc/src/DocBlock/Reference/CodeReference.php index c69b75e..1efe5a4 100644 --- a/libs/phpdoc/src/DocBlock/Reference/CodeReference.php +++ b/libs/phpdoc/src/DocBlock/Reference/CodeReference.php @@ -11,9 +11,9 @@ * References of this kind are always considered internal, so they are never * external. */ -abstract readonly class CodeReference implements ReferenceInterface +abstract class CodeReference implements ReferenceInterface { - public bool $isExternal; + public readonly bool $isExternal; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Reference/FunctionReference.php b/libs/phpdoc/src/DocBlock/Reference/FunctionReference.php index 17eb0e1..3c61f19 100644 --- a/libs/phpdoc/src/DocBlock/Reference/FunctionReference.php +++ b/libs/phpdoc/src/DocBlock/Reference/FunctionReference.php @@ -7,7 +7,7 @@ /** * A reference to a function. */ -final readonly class FunctionReference extends CodeReference +final class FunctionReference extends CodeReference { public function __construct( /** @@ -15,7 +15,7 @@ public function __construct( * * @var non-empty-string */ - public string $name, + public readonly string $name, ) { parent::__construct(); } diff --git a/libs/phpdoc/src/DocBlock/Reference/ReferenceInterface.php b/libs/phpdoc/src/DocBlock/Reference/ReferenceInterface.php index ceca2fa..68f1853 100644 --- a/libs/phpdoc/src/DocBlock/Reference/ReferenceInterface.php +++ b/libs/phpdoc/src/DocBlock/Reference/ReferenceInterface.php @@ -9,14 +9,8 @@ * * The target may either be an element of the described codebase or an * external resource located outside of it. + * + * @property-read bool $isExternal Gets {@see true} in case of the reference points outside of the described + * codebase, such as an external web page. */ -interface ReferenceInterface extends \Stringable -{ - /** - * Gets {@see true} when the reference points to a resource located - * outside of the described codebase, such as an external web page. - */ - public bool $isExternal { - get; - } -} +interface ReferenceInterface extends \Stringable {} diff --git a/libs/phpdoc/src/DocBlock/Reference/SymbolReference.php b/libs/phpdoc/src/DocBlock/Reference/SymbolReference.php index 4e1ca57..1ad3449 100644 --- a/libs/phpdoc/src/DocBlock/Reference/SymbolReference.php +++ b/libs/phpdoc/src/DocBlock/Reference/SymbolReference.php @@ -8,7 +8,7 @@ * A reference to a named symbol, such as a class, interface, trait, enum * or a global constant. */ -final readonly class SymbolReference extends CodeReference +final class SymbolReference extends CodeReference { public function __construct( /** @@ -16,7 +16,7 @@ public function __construct( * * @var non-empty-string */ - public string $name, + public readonly string $name, ) { parent::__construct(); } diff --git a/libs/phpdoc/src/DocBlock/Reference/TypeReference.php b/libs/phpdoc/src/DocBlock/Reference/TypeReference.php index 035584a..ab443cc 100644 --- a/libs/phpdoc/src/DocBlock/Reference/TypeReference.php +++ b/libs/phpdoc/src/DocBlock/Reference/TypeReference.php @@ -9,18 +9,18 @@ /** * A parsed type together with the exact source text it was read from. */ -final readonly class TypeReference implements ReferenceInterface +final class TypeReference implements ReferenceInterface { - public bool $isExternal; + public readonly bool $isExternal; public function __construct( - public TypeNode $type, + public readonly TypeNode $type, /** * The original type text, exactly as it appeared in the source. * * @var non-empty-string */ - public string $source, + public readonly string $source, ) { $this->isExternal = false; } diff --git a/libs/phpdoc/src/DocBlock/Reference/UriReference.php b/libs/phpdoc/src/DocBlock/Reference/UriReference.php index 0c2a6b4..3a1deab 100644 --- a/libs/phpdoc/src/DocBlock/Reference/UriReference.php +++ b/libs/phpdoc/src/DocBlock/Reference/UriReference.php @@ -11,9 +11,9 @@ * Such a reference always points outside the described codebase, so it * is always external. */ -readonly class UriReference implements ReferenceInterface +class UriReference implements ReferenceInterface { - public bool $isExternal; + public readonly bool $isExternal; public function __construct( /** @@ -21,7 +21,7 @@ public function __construct( * * @var non-empty-string */ - public string $uri, + public readonly string $uri, ) { $this->isExternal = true; } diff --git a/libs/phpdoc/src/DocBlock/Reference/UrlReference.php b/libs/phpdoc/src/DocBlock/Reference/UrlReference.php index 370504a..97228bd 100644 --- a/libs/phpdoc/src/DocBlock/Reference/UrlReference.php +++ b/libs/phpdoc/src/DocBlock/Reference/UrlReference.php @@ -8,4 +8,4 @@ * A reference to an external resource identified by a URL, that is a URI that * carries a scheme. */ -final readonly class UrlReference extends UriReference {} +final class UrlReference extends UriReference {} diff --git a/libs/phpdoc/src/DocBlock/Reference/VariableMethodReference.php b/libs/phpdoc/src/DocBlock/Reference/VariableMethodReference.php new file mode 100644 index 0000000..ff02f34 --- /dev/null +++ b/libs/phpdoc/src/DocBlock/Reference/VariableMethodReference.php @@ -0,0 +1,40 @@ +getItems() + * ``` + * + * The method is the one of the object the variable holds rather than the one + * a class declares, which is what a {@see ClassMethodReference} points to. + */ +final class VariableMethodReference extends CodeReference +{ + public function __construct( + /** + * The name of the variable holding the object, without the leading "$". + * + * @var non-empty-string + */ + public readonly string $variable, + /** + * The name of the referenced method. + * + * @var non-empty-string + */ + public readonly string $name, + ) { + parent::__construct(); + } + + public function __toString(): string + { + return \sprintf('$%s->%s()', $this->variable, $this->name); + } +} diff --git a/libs/phpdoc/src/DocBlock/Reference/VariablePropertyReference.php b/libs/phpdoc/src/DocBlock/Reference/VariablePropertyReference.php new file mode 100644 index 0000000..586c621 --- /dev/null +++ b/libs/phpdoc/src/DocBlock/Reference/VariablePropertyReference.php @@ -0,0 +1,40 @@ +items + * ``` + * + * The property is the one of the object the variable holds rather than the one + * a class declares, which is what a {@see ClassPropertyReference} points to. + */ +final class VariablePropertyReference extends CodeReference +{ + public function __construct( + /** + * The name of the variable holding the object, without the leading "$". + * + * @var non-empty-string + */ + public readonly string $variable, + /** + * The name of the referenced property. + * + * @var non-empty-string + */ + public readonly string $name, + ) { + parent::__construct(); + } + + public function __toString(): string + { + return \sprintf('$%s->%s', $this->variable, $this->name); + } +} diff --git a/libs/phpdoc/src/DocBlock/Reference/VariableReference.php b/libs/phpdoc/src/DocBlock/Reference/VariableReference.php index f5ad74c..5a1da14 100644 --- a/libs/phpdoc/src/DocBlock/Reference/VariableReference.php +++ b/libs/phpdoc/src/DocBlock/Reference/VariableReference.php @@ -7,7 +7,7 @@ /** * A reference to a variable. */ -final readonly class VariableReference extends CodeReference +final class VariableReference extends CodeReference { public function __construct( /** @@ -15,7 +15,7 @@ public function __construct( * * @var non-empty-string */ - public string $name, + public readonly string $name, ) { parent::__construct(); } diff --git a/libs/phpdoc/src/DocBlock/Tag/AbstractTag/AbstractTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/AbstractTag/AbstractTagDefinition.php index 1a46a0d..b7e8ff3 100644 --- a/libs/phpdoc/src/DocBlock/Tag/AbstractTag/AbstractTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/AbstractTag/AbstractTagDefinition.php @@ -20,7 +20,7 @@ */ final class AbstractTagDefinition extends TagDefinition { - public const string NAME = 'abstract'; + public const NAME = 'abstract'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/AccessTag/AccessTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/AccessTag/AccessTagDefinition.php index 67793e9..c062479 100644 --- a/libs/phpdoc/src/DocBlock/Tag/AccessTag/AccessTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/AccessTag/AccessTagDefinition.php @@ -22,7 +22,7 @@ */ final class AccessTagDefinition extends TagDefinition { - public const string NAME = 'access'; + public const NAME = 'access'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/AllowPrivateMutationTag/AllowPrivateMutationTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/AllowPrivateMutationTag/AllowPrivateMutationTagDefinition.php index 788323a..de0db8f 100644 --- a/libs/phpdoc/src/DocBlock/Tag/AllowPrivateMutationTag/AllowPrivateMutationTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/AllowPrivateMutationTag/AllowPrivateMutationTagDefinition.php @@ -21,7 +21,7 @@ */ final class AllowPrivateMutationTagDefinition extends TagDefinition { - public const string NAME = 'allow-private-mutation'; + public const NAME = 'allow-private-mutation'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ApiTag/ApiTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ApiTag/ApiTagDefinition.php index 0753cd5..9ef3a79 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ApiTag/ApiTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ApiTag/ApiTagDefinition.php @@ -21,7 +21,7 @@ */ final class ApiTagDefinition extends TagDefinition { - public const string NAME = 'api'; + public const NAME = 'api'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/AssertIfFalseTag/AssertIfFalseTag.php b/libs/phpdoc/src/DocBlock/Tag/AssertIfFalseTag/AssertIfFalseTag.php index 8d105de..2f84d06 100644 --- a/libs/phpdoc/src/DocBlock/Tag/AssertIfFalseTag/AssertIfFalseTag.php +++ b/libs/phpdoc/src/DocBlock/Tag/AssertIfFalseTag/AssertIfFalseTag.php @@ -4,10 +4,10 @@ namespace TypeLang\PhpDoc\DocBlock\Tag\AssertIfFalseTag; -use TypeLang\PhpDoc\DocBlock\Tag\TypedVariableTag; +use TypeLang\PhpDoc\DocBlock\Tag\AssertionTag; /** - * The `@assert-if-false` tag asserts the given type for a variable, but + * The `@assert-if-false` tag asserts the given type for a subject, but * only when the function returns `false`. */ -final class AssertIfFalseTag extends TypedVariableTag {} +final class AssertIfFalseTag extends AssertionTag {} diff --git a/libs/phpdoc/src/DocBlock/Tag/AssertIfFalseTag/AssertIfFalseTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/AssertIfFalseTag/AssertIfFalseTagDefinition.php index 5268326..3f5127d 100644 --- a/libs/phpdoc/src/DocBlock/Tag/AssertIfFalseTag/AssertIfFalseTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/AssertIfFalseTag/AssertIfFalseTagDefinition.php @@ -4,54 +4,36 @@ namespace TypeLang\PhpDoc\DocBlock\Tag\AssertIfFalseTag; -use TypeLang\PhpDoc\DocBlock\Combinator\DescriptionCombinator; -use TypeLang\PhpDoc\DocBlock\Combinator\TypeCombinator; -use TypeLang\PhpDoc\DocBlock\Combinator\VariableCombinator; use TypeLang\PhpDoc\DocBlock\Description\DescriptionInterface; +use TypeLang\PhpDoc\DocBlock\Reference\CodeReference; use TypeLang\PhpDoc\DocBlock\Reference\TypeReference; -use TypeLang\PhpDoc\DocBlock\TagDefinition\Spec; -use TypeLang\PhpDoc\DocBlock\TagDefinition\TagDefinition; -use TypeLang\PhpDoc\DocBlock\TagDefinition\TagPayload; -use TypeLang\PhpDoc\DocBlock\TagDefinition\TagPlacement; +use TypeLang\PhpDoc\DocBlock\Tag\AssertionTagDefinition; +use TypeLang\PhpDoc\DocBlock\Tag\AssertOperator; /** - * The `@assert-if-false` tag asserts the given type for a variable, but + * The `@assert-if-false` tag asserts the given type for a subject, but * only when the function returns `false`. * * ``` - * "@assert-if-false" [ ] + * "@assert-if-false" [ "!" | "=" | "!=" ] [ ] * ``` */ -final class AssertIfFalseTagDefinition extends TagDefinition +final class AssertIfFalseTagDefinition extends AssertionTagDefinition { - public const string NAME = 'assert-if-false'; + public const NAME = 'assert-if-false'; public function __construct() { - parent::__construct( - name: self::NAME, - spec: Spec::sequence( - Spec::rule(TypeCombinator::NAME, 'type'), - Spec::rule(VariableCombinator::NAME, 'variable'), - Spec::maybe( - Spec::rule(DescriptionCombinator::NAME, 'description'), - ), - ), - placement: TagPlacement::Block, - ); + parent::__construct(self::NAME); } - public function create(string $name, TagPayload $result): AssertIfFalseTag - { - /** @var TypeReference $type */ - $type = $result->get('type'); - - /** @var non-empty-string $variable */ - $variable = $result->get('variable'); - - /** @var DescriptionInterface|null $description */ - $description = $result->find('description'); - - return new AssertIfFalseTag($name, $type, $variable, $description); + protected function createAssertion( + string $name, + TypeReference $statement, + CodeReference $subject, + AssertOperator $operator, + ?DescriptionInterface $description, + ): AssertIfFalseTag { + return new AssertIfFalseTag($name, $statement, $subject, $operator, $description); } } diff --git a/libs/phpdoc/src/DocBlock/Tag/AssertIfTrueTag/AssertIfTrueTag.php b/libs/phpdoc/src/DocBlock/Tag/AssertIfTrueTag/AssertIfTrueTag.php index 91faebd..9c1e0e4 100644 --- a/libs/phpdoc/src/DocBlock/Tag/AssertIfTrueTag/AssertIfTrueTag.php +++ b/libs/phpdoc/src/DocBlock/Tag/AssertIfTrueTag/AssertIfTrueTag.php @@ -4,10 +4,10 @@ namespace TypeLang\PhpDoc\DocBlock\Tag\AssertIfTrueTag; -use TypeLang\PhpDoc\DocBlock\Tag\TypedVariableTag; +use TypeLang\PhpDoc\DocBlock\Tag\AssertionTag; /** - * The `@assert-if-true` tag asserts the given type for a variable, but + * The `@assert-if-true` tag asserts the given type for a subject, but * only when the function returns `true`. */ -final class AssertIfTrueTag extends TypedVariableTag {} +final class AssertIfTrueTag extends AssertionTag {} diff --git a/libs/phpdoc/src/DocBlock/Tag/AssertIfTrueTag/AssertIfTrueTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/AssertIfTrueTag/AssertIfTrueTagDefinition.php index 742049d..0513a66 100644 --- a/libs/phpdoc/src/DocBlock/Tag/AssertIfTrueTag/AssertIfTrueTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/AssertIfTrueTag/AssertIfTrueTagDefinition.php @@ -4,54 +4,36 @@ namespace TypeLang\PhpDoc\DocBlock\Tag\AssertIfTrueTag; -use TypeLang\PhpDoc\DocBlock\Combinator\DescriptionCombinator; -use TypeLang\PhpDoc\DocBlock\Combinator\TypeCombinator; -use TypeLang\PhpDoc\DocBlock\Combinator\VariableCombinator; use TypeLang\PhpDoc\DocBlock\Description\DescriptionInterface; +use TypeLang\PhpDoc\DocBlock\Reference\CodeReference; use TypeLang\PhpDoc\DocBlock\Reference\TypeReference; -use TypeLang\PhpDoc\DocBlock\TagDefinition\Spec; -use TypeLang\PhpDoc\DocBlock\TagDefinition\TagDefinition; -use TypeLang\PhpDoc\DocBlock\TagDefinition\TagPayload; -use TypeLang\PhpDoc\DocBlock\TagDefinition\TagPlacement; +use TypeLang\PhpDoc\DocBlock\Tag\AssertionTagDefinition; +use TypeLang\PhpDoc\DocBlock\Tag\AssertOperator; /** - * The `@assert-if-true` tag asserts the given type for a variable, but + * The `@assert-if-true` tag asserts the given type for a subject, but * only when the function returns `true`. * * ``` - * "@assert-if-true" [ ] + * "@assert-if-true" [ "!" | "=" | "!=" ] [ ] * ``` */ -final class AssertIfTrueTagDefinition extends TagDefinition +final class AssertIfTrueTagDefinition extends AssertionTagDefinition { - public const string NAME = 'assert-if-true'; + public const NAME = 'assert-if-true'; public function __construct() { - parent::__construct( - name: self::NAME, - spec: Spec::sequence( - Spec::rule(TypeCombinator::NAME, 'type'), - Spec::rule(VariableCombinator::NAME, 'variable'), - Spec::maybe( - Spec::rule(DescriptionCombinator::NAME, 'description'), - ), - ), - placement: TagPlacement::Block, - ); + parent::__construct(self::NAME); } - public function create(string $name, TagPayload $result): AssertIfTrueTag - { - /** @var TypeReference $type */ - $type = $result->get('type'); - - /** @var non-empty-string $variable */ - $variable = $result->get('variable'); - - /** @var DescriptionInterface|null $description */ - $description = $result->find('description'); - - return new AssertIfTrueTag($name, $type, $variable, $description); + protected function createAssertion( + string $name, + TypeReference $statement, + CodeReference $subject, + AssertOperator $operator, + ?DescriptionInterface $description, + ): AssertIfTrueTag { + return new AssertIfTrueTag($name, $statement, $subject, $operator, $description); } } diff --git a/libs/phpdoc/src/DocBlock/Tag/AssertOperator.php b/libs/phpdoc/src/DocBlock/Tag/AssertOperator.php new file mode 100644 index 0000000..5e4ef96 --- /dev/null +++ b/libs/phpdoc/src/DocBlock/Tag/AssertOperator.php @@ -0,0 +1,42 @@ + [ ] + * "@assert" [ "!" | "=" | "!=" ] [ ] * ``` */ -final class AssertTagDefinition extends TagDefinition +final class AssertTagDefinition extends AssertionTagDefinition { - public const string NAME = 'assert'; + public const NAME = 'assert'; public function __construct() { - parent::__construct( - name: self::NAME, - spec: Spec::sequence( - Spec::rule(TypeCombinator::NAME, 'type'), - Spec::rule(VariableCombinator::NAME, 'variable'), - Spec::maybe( - Spec::rule(DescriptionCombinator::NAME, 'description'), - ), - ), - placement: TagPlacement::Block, - ); + parent::__construct(self::NAME); } - public function create(string $name, TagPayload $result): AssertTag - { - /** @var TypeReference $type */ - $type = $result->get('type'); - - /** @var non-empty-string $variable */ - $variable = $result->get('variable'); - - /** @var DescriptionInterface|null $description */ - $description = $result->find('description'); - - return new AssertTag($name, $type, $variable, $description); + protected function createAssertion( + string $name, + TypeReference $statement, + CodeReference $subject, + AssertOperator $operator, + ?DescriptionInterface $description, + ): AssertTag { + return new AssertTag($name, $statement, $subject, $operator, $description); } } diff --git a/libs/phpdoc/src/DocBlock/Tag/AssertionTag.php b/libs/phpdoc/src/DocBlock/Tag/AssertionTag.php new file mode 100644 index 0000000..913d5bd --- /dev/null +++ b/libs/phpdoc/src/DocBlock/Tag/AssertionTag.php @@ -0,0 +1,58 @@ + [ ] + * ``` + */ +abstract class AssertionTag extends TypedTag +{ + public function __construct( + string $name, + TypeReference $statement, + /** + * What the assertion is written of. + */ + public readonly ReferenceInterface $subject, + /** + * The way the subject relates to the type. + */ + public readonly AssertOperator $operator = AssertOperator::Is, + ?DescriptionInterface $description = null, + ) { + parent::__construct($name, $statement, $description); + } + + #[\Override] + public function __toString(): string + { + $result = \sprintf( + '@%s %s%s %s', + $this->name, + $this->operator->value, + $this->statement, + $this->subject, + ); + + if ($this->description !== null) { + $result .= ' ' . $this->description; + } + + return $result; + } +} diff --git a/libs/phpdoc/src/DocBlock/Tag/AssertionTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/AssertionTagDefinition.php new file mode 100644 index 0000000..706f442 --- /dev/null +++ b/libs/phpdoc/src/DocBlock/Tag/AssertionTagDefinition.php @@ -0,0 +1,113 @@ + [ ] + * "@assert-if-true" [ "!" | "=" | "!=" ] [ ] + * "@assert-if-false" [ "!" | "=" | "!=" ] [ ] + * ``` + */ +abstract class AssertionTagDefinition extends TagDefinition +{ + /** + * The names the prefix of the type is reported under, one per prefix. + * + * @var non-empty-string + */ + private const OPERATOR_IS_NOT = 'is-not'; + + /** + * @var non-empty-string + */ + private const OPERATOR_EQUALS = 'equals'; + + /** + * @var non-empty-string + */ + private const OPERATOR_NOT_EQUALS = 'not-equals'; + + /** + * @param non-empty-string $name + */ + public function __construct(string $name) + { + parent::__construct( + name: $name, + spec: Spec::sequence( + // A literal reports nothing but its own presence, so each of + // the prefixes is asked about under a name of its own. The + // longest one is offered first, so that the "!" of a "!=" is + // not taken for the whole of it. + Spec::maybe( + Spec::oneOf( + Spec::literal(AssertOperator::NotEquals->value, self::OPERATOR_NOT_EQUALS), + Spec::literal(AssertOperator::IsNot->value, self::OPERATOR_IS_NOT), + Spec::literal(AssertOperator::Equals->value, self::OPERATOR_EQUALS), + ), + ), + Spec::rule(TypeCombinator::NAME, 'type'), + Spec::rule(AssertSubjectCombinator::NAME, 'subject'), + Spec::maybe( + Spec::rule(DescriptionCombinator::NAME, 'description'), + ), + ), + placement: TagPlacement::Block, + ); + } + + final public function create(string $name, TagPayload $result): AssertionTag + { + /** @var TypeReference $type */ + $type = $result->get('type'); + + /** @var CodeReference $subject */ + $subject = $result->get('subject'); + + /** @var DescriptionInterface|null $description */ + $description = $result->find('description'); + + return $this->createAssertion( + name: $name, + statement: $type, + subject: $subject, + operator: match (true) { + $result->find(self::OPERATOR_NOT_EQUALS) !== null => AssertOperator::NotEquals, + $result->find(self::OPERATOR_IS_NOT) !== null => AssertOperator::IsNot, + $result->find(self::OPERATOR_EQUALS) !== null => AssertOperator::Equals, + default => AssertOperator::Is, + }, + description: $description, + ); + } + + /** + * @param non-empty-string $name + */ + abstract protected function createAssertion( + string $name, + TypeReference $statement, + CodeReference $subject, + AssertOperator $operator, + ?DescriptionInterface $description, + ): AssertionTag; +} diff --git a/libs/phpdoc/src/DocBlock/Tag/AuthorTag/AuthorTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/AuthorTag/AuthorTagDefinition.php index 92c97b9..95f3c3d 100644 --- a/libs/phpdoc/src/DocBlock/Tag/AuthorTag/AuthorTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/AuthorTag/AuthorTagDefinition.php @@ -23,7 +23,7 @@ */ final class AuthorTagDefinition extends TagDefinition { - public const string NAME = 'author'; + public const NAME = 'author'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/CategoryTag/CategoryTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/CategoryTag/CategoryTagDefinition.php index 84eb4b4..9d67223 100644 --- a/libs/phpdoc/src/DocBlock/Tag/CategoryTag/CategoryTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/CategoryTag/CategoryTagDefinition.php @@ -20,7 +20,7 @@ */ final class CategoryTagDefinition extends TagDefinition { - public const string NAME = 'category'; + public const NAME = 'category'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/CheckTypeTag/CheckTypeTag.php b/libs/phpdoc/src/DocBlock/Tag/CheckTypeTag/CheckTypeTag.php index 8d7da6a..a019ba7 100644 --- a/libs/phpdoc/src/DocBlock/Tag/CheckTypeTag/CheckTypeTag.php +++ b/libs/phpdoc/src/DocBlock/Tag/CheckTypeTag/CheckTypeTag.php @@ -13,9 +13,10 @@ */ abstract class CheckTypeTag extends Tag { - public TypeNode $type { - get => $this->statement->type; - } + /** + * The type declared by the tag. + */ + public readonly TypeNode $type; public function __construct( string $name, @@ -30,6 +31,8 @@ public function __construct( */ protected readonly TypeReference $statement, ) { + $this->type = $statement->type; + parent::__construct($name); } diff --git a/libs/phpdoc/src/DocBlock/Tag/CheckTypeTag/PsalmCheckTypeExactTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/CheckTypeTag/PsalmCheckTypeExactTagDefinition.php index c13319f..8c486f0 100644 --- a/libs/phpdoc/src/DocBlock/Tag/CheckTypeTag/PsalmCheckTypeExactTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/CheckTypeTag/PsalmCheckTypeExactTagDefinition.php @@ -16,7 +16,7 @@ */ final class PsalmCheckTypeExactTagDefinition extends CheckTypeTagDefinition { - public const string NAME = 'psalm-check-type-exact'; + public const NAME = 'psalm-check-type-exact'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/CheckTypeTag/PsalmCheckTypeTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/CheckTypeTag/PsalmCheckTypeTagDefinition.php index 9adeda8..952cc2c 100644 --- a/libs/phpdoc/src/DocBlock/Tag/CheckTypeTag/PsalmCheckTypeTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/CheckTypeTag/PsalmCheckTypeTagDefinition.php @@ -16,7 +16,7 @@ */ final class PsalmCheckTypeTagDefinition extends CheckTypeTagDefinition { - public const string NAME = 'psalm-check-type'; + public const NAME = 'psalm-check-type'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreEndTag/CodingStandardsIgnoreEndTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreEndTag/CodingStandardsIgnoreEndTagDefinition.php index 65185d4..d6d98e6 100644 --- a/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreEndTag/CodingStandardsIgnoreEndTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreEndTag/CodingStandardsIgnoreEndTagDefinition.php @@ -22,7 +22,7 @@ */ final class CodingStandardsIgnoreEndTagDefinition extends TagDefinition { - public const string NAME = 'codingStandardsIgnoreEnd'; + public const NAME = 'codingStandardsIgnoreEnd'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreFileTag/CodingStandardsIgnoreFileTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreFileTag/CodingStandardsIgnoreFileTagDefinition.php index 5104d5b..9ccf066 100644 --- a/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreFileTag/CodingStandardsIgnoreFileTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreFileTag/CodingStandardsIgnoreFileTagDefinition.php @@ -21,7 +21,7 @@ */ final class CodingStandardsIgnoreFileTagDefinition extends TagDefinition { - public const string NAME = 'codingStandardsIgnoreFile'; + public const NAME = 'codingStandardsIgnoreFile'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreLineTag/CodingStandardsIgnoreLineTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreLineTag/CodingStandardsIgnoreLineTagDefinition.php index fb4c8ef..cbea03d 100644 --- a/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreLineTag/CodingStandardsIgnoreLineTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreLineTag/CodingStandardsIgnoreLineTagDefinition.php @@ -21,7 +21,7 @@ */ final class CodingStandardsIgnoreLineTagDefinition extends TagDefinition { - public const string NAME = 'codingStandardsIgnoreLine'; + public const NAME = 'codingStandardsIgnoreLine'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreStartTag/CodingStandardsIgnoreStartTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreStartTag/CodingStandardsIgnoreStartTagDefinition.php index c5670a1..e882441 100644 --- a/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreStartTag/CodingStandardsIgnoreStartTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/CodingStandardsIgnoreStartTag/CodingStandardsIgnoreStartTagDefinition.php @@ -22,7 +22,7 @@ */ final class CodingStandardsIgnoreStartTagDefinition extends TagDefinition { - public const string NAME = 'codingStandardsIgnoreStart'; + public const NAME = 'codingStandardsIgnoreStart'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/CodingStandardsTag/CodingStandardsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/CodingStandardsTag/CodingStandardsTagDefinition.php index 3d918f3..8ad792e 100644 --- a/libs/phpdoc/src/DocBlock/Tag/CodingStandardsTag/CodingStandardsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/CodingStandardsTag/CodingStandardsTagDefinition.php @@ -21,7 +21,7 @@ */ final class CodingStandardsTagDefinition extends TagDefinition { - public const string NAME = 'codingStandards'; + public const NAME = 'codingStandards'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ConsistentConstructorTag/ConsistentConstructorTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ConsistentConstructorTag/ConsistentConstructorTagDefinition.php index 2fed870..918a432 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ConsistentConstructorTag/ConsistentConstructorTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ConsistentConstructorTag/ConsistentConstructorTagDefinition.php @@ -21,7 +21,7 @@ */ final class ConsistentConstructorTagDefinition extends TagDefinition { - public const string NAME = 'consistent-constructor'; + public const NAME = 'consistent-constructor'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/CopyrightTag/CopyrightTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/CopyrightTag/CopyrightTagDefinition.php index 96aaa45..bdf5dfb 100644 --- a/libs/phpdoc/src/DocBlock/Tag/CopyrightTag/CopyrightTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/CopyrightTag/CopyrightTagDefinition.php @@ -20,7 +20,7 @@ */ final class CopyrightTagDefinition extends TagDefinition { - public const string NAME = 'copyright'; + public const NAME = 'copyright'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/DeprecatedTag/DeprecatedTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/DeprecatedTag/DeprecatedTagDefinition.php index fbd8989..8a44383 100644 --- a/libs/phpdoc/src/DocBlock/Tag/DeprecatedTag/DeprecatedTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/DeprecatedTag/DeprecatedTagDefinition.php @@ -22,7 +22,7 @@ */ final class DeprecatedTagDefinition extends TagDefinition { - public const string NAME = 'deprecated'; + public const NAME = 'deprecated'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ExampleTag/ExampleTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ExampleTag/ExampleTagDefinition.php index 6f5a1d8..b7b072f 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ExampleTag/ExampleTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ExampleTag/ExampleTagDefinition.php @@ -25,7 +25,7 @@ */ final class ExampleTagDefinition extends TagDefinition { - public const string NAME = 'example'; + public const NAME = 'example'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ExpectedExceptionTag/ExpectedExceptionTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ExpectedExceptionTag/ExpectedExceptionTagDefinition.php index 3aa3b74..40fb4e3 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ExpectedExceptionTag/ExpectedExceptionTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ExpectedExceptionTag/ExpectedExceptionTagDefinition.php @@ -23,7 +23,7 @@ */ final class ExpectedExceptionTagDefinition extends TagDefinition { - public const string NAME = 'expectedException'; + public const NAME = 'expectedException'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/FilesourceTag/FilesourceTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/FilesourceTag/FilesourceTagDefinition.php index 423a45a..234af14 100644 --- a/libs/phpdoc/src/DocBlock/Tag/FilesourceTag/FilesourceTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/FilesourceTag/FilesourceTagDefinition.php @@ -21,7 +21,7 @@ */ final class FilesourceTagDefinition extends TagDefinition { - public const string NAME = 'filesource'; + public const NAME = 'filesource'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/FinalTag/FinalTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/FinalTag/FinalTagDefinition.php index a8308f3..b98ff05 100644 --- a/libs/phpdoc/src/DocBlock/Tag/FinalTag/FinalTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/FinalTag/FinalTagDefinition.php @@ -21,7 +21,7 @@ */ final class FinalTagDefinition extends TagDefinition { - public const string NAME = 'final'; + public const NAME = 'final'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/FormatterOffTag/FormatterOffTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/FormatterOffTag/FormatterOffTagDefinition.php index 543e06b..bec728d 100644 --- a/libs/phpdoc/src/DocBlock/Tag/FormatterOffTag/FormatterOffTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/FormatterOffTag/FormatterOffTagDefinition.php @@ -22,7 +22,7 @@ */ final class FormatterOffTagDefinition extends TagDefinition { - public const string NAME = 'formatter:off'; + public const NAME = 'formatter:off'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/FormatterOnTag/FormatterOnTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/FormatterOnTag/FormatterOnTagDefinition.php index 6720214..f8c5024 100644 --- a/libs/phpdoc/src/DocBlock/Tag/FormatterOnTag/FormatterOnTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/FormatterOnTag/FormatterOnTagDefinition.php @@ -21,7 +21,7 @@ */ final class FormatterOnTagDefinition extends TagDefinition { - public const string NAME = 'formatter:on'; + public const NAME = 'formatter:on'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/GenericTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/GenericTagDefinition.php index bda1c5e..aef8434 100644 --- a/libs/phpdoc/src/DocBlock/Tag/GenericTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/GenericTagDefinition.php @@ -13,7 +13,7 @@ final class GenericTagDefinition extends TagDefinition { - public const string NAME = ''; + public const NAME = ''; /** * @param TagPlacement $placement generic (unknown) tag has no dedicated diff --git a/libs/phpdoc/src/DocBlock/Tag/GlobalTag/GlobalTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/GlobalTag/GlobalTagDefinition.php index 2c1d04e..566a026 100644 --- a/libs/phpdoc/src/DocBlock/Tag/GlobalTag/GlobalTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/GlobalTag/GlobalTagDefinition.php @@ -23,7 +23,7 @@ */ final class GlobalTagDefinition extends TagDefinition { - public const string NAME = 'global'; + public const NAME = 'global'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/IgnoreTag/IgnoreTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/IgnoreTag/IgnoreTagDefinition.php index 523215f..e6d2829 100644 --- a/libs/phpdoc/src/DocBlock/Tag/IgnoreTag/IgnoreTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/IgnoreTag/IgnoreTagDefinition.php @@ -21,7 +21,7 @@ */ final class IgnoreTagDefinition extends TagDefinition { - public const string NAME = 'ignore'; + public const NAME = 'ignore'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ImmutableTag/ImmutableTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ImmutableTag/ImmutableTagDefinition.php index b0f94dd..20f9b6a 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ImmutableTag/ImmutableTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ImmutableTag/ImmutableTagDefinition.php @@ -21,7 +21,7 @@ */ final class ImmutableTagDefinition extends TagDefinition { - public const string NAME = 'immutable'; + public const NAME = 'immutable'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ImportTypeAliasTag/ImportTypeAliasTag.php b/libs/phpdoc/src/DocBlock/Tag/ImportTypeAliasTag/ImportTypeAliasTag.php index 556274d..0aa46f5 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ImportTypeAliasTag/ImportTypeAliasTag.php +++ b/libs/phpdoc/src/DocBlock/Tag/ImportTypeAliasTag/ImportTypeAliasTag.php @@ -14,9 +14,10 @@ */ final class ImportTypeAliasTag extends Tag { - public TypeNode $type { - get => $this->statement->type; - } + /** + * The type declared by the tag. + */ + public readonly TypeNode $type; public function __construct( string $name, @@ -37,6 +38,8 @@ public function __construct( */ public readonly ?string $as = null, ) { + $this->type = $statement->type; + parent::__construct($name); } diff --git a/libs/phpdoc/src/DocBlock/Tag/ImportTypeAliasTag/ImportTypeAliasTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ImportTypeAliasTag/ImportTypeAliasTagDefinition.php index c8b3219..cd1a2d7 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ImportTypeAliasTag/ImportTypeAliasTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ImportTypeAliasTag/ImportTypeAliasTagDefinition.php @@ -22,7 +22,7 @@ */ final class ImportTypeAliasTagDefinition extends TagDefinition { - public const string NAME = 'import-type'; + public const NAME = 'import-type'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/InheritDocTag/InheritDocTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/InheritDocTag/InheritDocTagDefinition.php index dfb02b9..1b99d3c 100644 --- a/libs/phpdoc/src/DocBlock/Tag/InheritDocTag/InheritDocTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/InheritDocTag/InheritDocTagDefinition.php @@ -20,7 +20,7 @@ */ final class InheritDocTagDefinition extends TagDefinition { - public const string NAME = 'inheritDoc'; + public const NAME = 'inheritDoc'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/InheritanceTag/ExtendsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/InheritanceTag/ExtendsTagDefinition.php index 230c40f..c2b6790 100644 --- a/libs/phpdoc/src/DocBlock/Tag/InheritanceTag/ExtendsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/InheritanceTag/ExtendsTagDefinition.php @@ -19,7 +19,7 @@ */ final class ExtendsTagDefinition extends InheritanceTagDefinition { - public const string NAME = 'extends'; + public const NAME = 'extends'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/InheritanceTag/ImplementsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/InheritanceTag/ImplementsTagDefinition.php index bb3ddd6..fbedd34 100644 --- a/libs/phpdoc/src/DocBlock/Tag/InheritanceTag/ImplementsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/InheritanceTag/ImplementsTagDefinition.php @@ -18,7 +18,7 @@ */ final class ImplementsTagDefinition extends InheritanceTagDefinition { - public const string NAME = 'implements'; + public const NAME = 'implements'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/InheritanceTag/UseTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/InheritanceTag/UseTagDefinition.php index 83b4756..cf59e52 100644 --- a/libs/phpdoc/src/DocBlock/Tag/InheritanceTag/UseTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/InheritanceTag/UseTagDefinition.php @@ -18,7 +18,7 @@ */ final class UseTagDefinition extends InheritanceTagDefinition { - public const string NAME = 'use'; + public const NAME = 'use'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/InternalTag/InternalTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/InternalTag/InternalTagDefinition.php index 3f65f81..a0bd5b5 100644 --- a/libs/phpdoc/src/DocBlock/Tag/InternalTag/InternalTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/InternalTag/InternalTagDefinition.php @@ -21,7 +21,7 @@ */ final class InternalTagDefinition extends TagDefinition { - public const string NAME = 'internal'; + public const NAME = 'internal'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/LanguageTag/LanguageTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/LanguageTag/LanguageTagDefinition.php index 9a8d454..9a2d664 100644 --- a/libs/phpdoc/src/DocBlock/Tag/LanguageTag/LanguageTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/LanguageTag/LanguageTagDefinition.php @@ -23,7 +23,7 @@ */ final class LanguageTagDefinition extends TagDefinition { - public const string NAME = 'language'; + public const NAME = 'language'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/LicenseTag/LicenseTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/LicenseTag/LicenseTagDefinition.php index 1023555..684316b 100644 --- a/libs/phpdoc/src/DocBlock/Tag/LicenseTag/LicenseTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/LicenseTag/LicenseTagDefinition.php @@ -24,7 +24,7 @@ */ final class LicenseTagDefinition extends TagDefinition { - public const string NAME = 'license'; + public const NAME = 'license'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/LinkTag/LinkTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/LinkTag/LinkTagDefinition.php index 1326ceb..2b2133e 100644 --- a/libs/phpdoc/src/DocBlock/Tag/LinkTag/LinkTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/LinkTag/LinkTagDefinition.php @@ -30,7 +30,7 @@ */ final class LinkTagDefinition extends TagDefinition { - public const string NAME = 'link'; + public const NAME = 'link'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/MethodTag/MethodTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/MethodTag/MethodTagDefinition.php index 7a96379..60aa619 100644 --- a/libs/phpdoc/src/DocBlock/Tag/MethodTag/MethodTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/MethodTag/MethodTagDefinition.php @@ -28,7 +28,7 @@ */ final class MethodTagDefinition extends TagDefinition { - public const string NAME = 'method'; + public const NAME = 'method'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/MixinTag/MixinTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/MixinTag/MixinTagDefinition.php index 7f710b6..5a7fcf2 100644 --- a/libs/phpdoc/src/DocBlock/Tag/MixinTag/MixinTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/MixinTag/MixinTagDefinition.php @@ -23,7 +23,7 @@ */ final class MixinTagDefinition extends TagDefinition { - public const string NAME = 'mixin'; + public const NAME = 'mixin'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/NameTag/NameTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/NameTag/NameTagDefinition.php index f23cbcb..021bf23 100644 --- a/libs/phpdoc/src/DocBlock/Tag/NameTag/NameTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/NameTag/NameTagDefinition.php @@ -21,7 +21,7 @@ */ final class NameTagDefinition extends TagDefinition { - public const string NAME = 'name'; + public const NAME = 'name'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/NoNamedArgumentsTag/NoNamedArgumentsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/NoNamedArgumentsTag/NoNamedArgumentsTagDefinition.php index 3f90e21..09d804b 100644 --- a/libs/phpdoc/src/DocBlock/Tag/NoNamedArgumentsTag/NoNamedArgumentsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/NoNamedArgumentsTag/NoNamedArgumentsTagDefinition.php @@ -21,7 +21,7 @@ */ final class NoNamedArgumentsTagDefinition extends TagDefinition { - public const string NAME = 'no-named-arguments'; + public const NAME = 'no-named-arguments'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/NoinspectionTag/NoinspectionTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/NoinspectionTag/NoinspectionTagDefinition.php index e4fb5ae..3ba4c21 100644 --- a/libs/phpdoc/src/DocBlock/Tag/NoinspectionTag/NoinspectionTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/NoinspectionTag/NoinspectionTagDefinition.php @@ -22,7 +22,7 @@ */ final class NoinspectionTagDefinition extends TagDefinition { - public const string NAME = 'noinspection'; + public const NAME = 'noinspection'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/NotDeprecatedTag/NotDeprecatedTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/NotDeprecatedTag/NotDeprecatedTagDefinition.php index e07ddb6..bf6a953 100644 --- a/libs/phpdoc/src/DocBlock/Tag/NotDeprecatedTag/NotDeprecatedTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/NotDeprecatedTag/NotDeprecatedTagDefinition.php @@ -21,7 +21,7 @@ */ final class NotDeprecatedTagDefinition extends TagDefinition { - public const string NAME = 'not-deprecated'; + public const NAME = 'not-deprecated'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/OverrideTag/OverrideTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/OverrideTag/OverrideTagDefinition.php index 4c825d0..d4c95e2 100644 --- a/libs/phpdoc/src/DocBlock/Tag/OverrideTag/OverrideTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/OverrideTag/OverrideTagDefinition.php @@ -20,7 +20,7 @@ */ final class OverrideTagDefinition extends TagDefinition { - public const string NAME = 'override'; + public const NAME = 'override'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PackageTag/PackageTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PackageTag/PackageTagDefinition.php index 32218eb..73224c0 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PackageTag/PackageTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PackageTag/PackageTagDefinition.php @@ -20,7 +20,7 @@ */ final class PackageTagDefinition extends TagDefinition { - public const string NAME = 'package'; + public const NAME = 'package'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ParamClosureThisTag/ParamClosureThisTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ParamClosureThisTag/ParamClosureThisTagDefinition.php index e5fed4f..e3ea085 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ParamClosureThisTag/ParamClosureThisTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ParamClosureThisTag/ParamClosureThisTagDefinition.php @@ -24,7 +24,7 @@ */ final class ParamClosureThisTagDefinition extends TagDefinition { - public const string NAME = 'param-closure-this'; + public const NAME = 'param-closure-this'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ParamInvokedCallableTag/ParamImmediatelyInvokedCallableTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ParamInvokedCallableTag/ParamImmediatelyInvokedCallableTagDefinition.php index b55daa2..25ab37f 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ParamInvokedCallableTag/ParamImmediatelyInvokedCallableTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ParamInvokedCallableTag/ParamImmediatelyInvokedCallableTagDefinition.php @@ -16,7 +16,7 @@ */ final class ParamImmediatelyInvokedCallableTagDefinition extends ParamInvokedCallableTagDefinition { - public const string NAME = 'param-immediately-invoked-callable'; + public const NAME = 'param-immediately-invoked-callable'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ParamInvokedCallableTag/ParamLaterInvokedCallableTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ParamInvokedCallableTag/ParamLaterInvokedCallableTagDefinition.php index a5493bd..223bfb7 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ParamInvokedCallableTag/ParamLaterInvokedCallableTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ParamInvokedCallableTag/ParamLaterInvokedCallableTagDefinition.php @@ -16,7 +16,7 @@ */ final class ParamLaterInvokedCallableTagDefinition extends ParamInvokedCallableTagDefinition { - public const string NAME = 'param-later-invoked-callable'; + public const NAME = 'param-later-invoked-callable'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ParamOutTag/ParamOutTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ParamOutTag/ParamOutTagDefinition.php index 2e604e4..a4e9765 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ParamOutTag/ParamOutTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ParamOutTag/ParamOutTagDefinition.php @@ -24,7 +24,7 @@ */ final class ParamOutTagDefinition extends TagDefinition { - public const string NAME = 'param-out'; + public const NAME = 'param-out'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ParamTag/ParamTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ParamTag/ParamTagDefinition.php index 9eb794b..b736349 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ParamTag/ParamTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ParamTag/ParamTagDefinition.php @@ -26,7 +26,7 @@ */ final class ParamTagDefinition extends TagDefinition { - public const string NAME = 'param'; + public const NAME = 'param'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanClosureScopeTag/PhanClosureScopeTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanClosureScopeTag/PhanClosureScopeTagDefinition.php index 40c0ed7..9269a5a 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanClosureScopeTag/PhanClosureScopeTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanClosureScopeTag/PhanClosureScopeTagDefinition.php @@ -24,7 +24,7 @@ */ final class PhanClosureScopeTagDefinition extends TagDefinition { - public const string NAME = 'phan-closure-scope'; + public const NAME = 'phan-closure-scope'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanConstructorUsedForSideEffectsTag/PhanConstructorUsedForSideEffectsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanConstructorUsedForSideEffectsTag/PhanConstructorUsedForSideEffectsTagDefinition.php index 85db0ba..fb71dc0 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanConstructorUsedForSideEffectsTag/PhanConstructorUsedForSideEffectsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanConstructorUsedForSideEffectsTag/PhanConstructorUsedForSideEffectsTagDefinition.php @@ -22,7 +22,7 @@ */ final class PhanConstructorUsedForSideEffectsTagDefinition extends TagDefinition { - public const string NAME = 'phan-constructor-used-for-side-effects'; + public const NAME = 'phan-constructor-used-for-side-effects'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanFileSuppressTag/PhanFileSuppressTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanFileSuppressTag/PhanFileSuppressTagDefinition.php index 1f0b868..29bc1c1 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanFileSuppressTag/PhanFileSuppressTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanFileSuppressTag/PhanFileSuppressTagDefinition.php @@ -22,7 +22,7 @@ */ final class PhanFileSuppressTagDefinition extends TagDefinition { - public const string NAME = 'phan-file-suppress'; + public const NAME = 'phan-file-suppress'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanForbidUndeclaredMagicMethodsTag/PhanForbidUndeclaredMagicMethodsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanForbidUndeclaredMagicMethodsTag/PhanForbidUndeclaredMagicMethodsTagDefinition.php index c5ba0ba..ab65384 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanForbidUndeclaredMagicMethodsTag/PhanForbidUndeclaredMagicMethodsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanForbidUndeclaredMagicMethodsTag/PhanForbidUndeclaredMagicMethodsTagDefinition.php @@ -22,7 +22,7 @@ */ final class PhanForbidUndeclaredMagicMethodsTagDefinition extends TagDefinition { - public const string NAME = 'phan-forbid-undeclared-magic-methods'; + public const NAME = 'phan-forbid-undeclared-magic-methods'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanForbidUndeclaredMagicPropertiesTag/PhanForbidUndeclaredMagicPropertiesTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanForbidUndeclaredMagicPropertiesTag/PhanForbidUndeclaredMagicPropertiesTagDefinition.php index fac4c0b..9fb76d0 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanForbidUndeclaredMagicPropertiesTag/PhanForbidUndeclaredMagicPropertiesTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanForbidUndeclaredMagicPropertiesTag/PhanForbidUndeclaredMagicPropertiesTagDefinition.php @@ -22,7 +22,7 @@ */ final class PhanForbidUndeclaredMagicPropertiesTagDefinition extends TagDefinition { - public const string NAME = 'phan-forbid-undeclared-magic-properties'; + public const NAME = 'phan-forbid-undeclared-magic-properties'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanHardcodeReturnTypeTag/PhanHardcodeReturnTypeTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanHardcodeReturnTypeTag/PhanHardcodeReturnTypeTagDefinition.php index 7f14d6d..77c74d5 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanHardcodeReturnTypeTag/PhanHardcodeReturnTypeTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanHardcodeReturnTypeTag/PhanHardcodeReturnTypeTagDefinition.php @@ -23,7 +23,7 @@ */ final class PhanHardcodeReturnTypeTagDefinition extends TagDefinition { - public const string NAME = 'phan-hardcode-return-type'; + public const NAME = 'phan-hardcode-return-type'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanOutputReferenceTag/PhanOutputReferenceTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanOutputReferenceTag/PhanOutputReferenceTagDefinition.php index cf21336..f3d3a4e 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanOutputReferenceTag/PhanOutputReferenceTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanOutputReferenceTag/PhanOutputReferenceTagDefinition.php @@ -21,7 +21,7 @@ */ final class PhanOutputReferenceTagDefinition extends TagDefinition { - public const string NAME = 'phan-output-reference'; + public const NAME = 'phan-output-reference'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanRealReturnTag/PhanRealReturnTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanRealReturnTag/PhanRealReturnTagDefinition.php index e7808f2..919b5bc 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanRealReturnTag/PhanRealReturnTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanRealReturnTag/PhanRealReturnTagDefinition.php @@ -23,7 +23,7 @@ */ final class PhanRealReturnTagDefinition extends TagDefinition { - public const string NAME = 'phan-real-return'; + public const NAME = 'phan-real-return'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanSideEffectFreeTag/PhanSideEffectFreeTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanSideEffectFreeTag/PhanSideEffectFreeTagDefinition.php index 869975a..5c2ed04 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanSideEffectFreeTag/PhanSideEffectFreeTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanSideEffectFreeTag/PhanSideEffectFreeTagDefinition.php @@ -21,7 +21,7 @@ */ final class PhanSideEffectFreeTagDefinition extends TagDefinition { - public const string NAME = 'phan-side-effect-free'; + public const NAME = 'phan-side-effect-free'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanSuppressCurrentLineTag/PhanSuppressCurrentLineTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanSuppressCurrentLineTag/PhanSuppressCurrentLineTagDefinition.php index 4b2ec7e..523c22b 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanSuppressCurrentLineTag/PhanSuppressCurrentLineTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanSuppressCurrentLineTag/PhanSuppressCurrentLineTagDefinition.php @@ -22,7 +22,7 @@ */ final class PhanSuppressCurrentLineTagDefinition extends TagDefinition { - public const string NAME = 'phan-suppress-current-line'; + public const NAME = 'phan-suppress-current-line'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanSuppressNextLineTag/PhanSuppressNextLineTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanSuppressNextLineTag/PhanSuppressNextLineTagDefinition.php index 6b6cabe..63d7fbb 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanSuppressNextLineTag/PhanSuppressNextLineTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanSuppressNextLineTag/PhanSuppressNextLineTagDefinition.php @@ -22,7 +22,7 @@ */ final class PhanSuppressNextLineTagDefinition extends TagDefinition { - public const string NAME = 'phan-suppress-next-line'; + public const NAME = 'phan-suppress-next-line'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanSuppressNextNextLineTag/PhanSuppressNextNextLineTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanSuppressNextNextLineTag/PhanSuppressNextNextLineTagDefinition.php index f35469f..4f4d7f5 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanSuppressNextNextLineTag/PhanSuppressNextNextLineTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanSuppressNextNextLineTag/PhanSuppressNextNextLineTagDefinition.php @@ -22,7 +22,7 @@ */ final class PhanSuppressNextNextLineTagDefinition extends TagDefinition { - public const string NAME = 'phan-suppress-next-next-line'; + public const NAME = 'phan-suppress-next-next-line'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanSuppressPreviousLineTag/PhanSuppressPreviousLineTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanSuppressPreviousLineTag/PhanSuppressPreviousLineTagDefinition.php index dd853d1..d6a7384 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanSuppressPreviousLineTag/PhanSuppressPreviousLineTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanSuppressPreviousLineTag/PhanSuppressPreviousLineTagDefinition.php @@ -22,7 +22,7 @@ */ final class PhanSuppressPreviousLineTagDefinition extends TagDefinition { - public const string NAME = 'phan-suppress-previous-line'; + public const NAME = 'phan-suppress-previous-line'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanTransientTag/PhanTransientTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanTransientTag/PhanTransientTagDefinition.php index 0790961..58ba93f 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanTransientTag/PhanTransientTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanTransientTag/PhanTransientTagDefinition.php @@ -21,7 +21,7 @@ */ final class PhanTransientTagDefinition extends TagDefinition { - public const string NAME = 'phan-transient'; + public const NAME = 'phan-transient'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhanWriteOnlyTag/PhanWriteOnlyTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhanWriteOnlyTag/PhanWriteOnlyTagDefinition.php index a2d8f99..6308893 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhanWriteOnlyTag/PhanWriteOnlyTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhanWriteOnlyTag/PhanWriteOnlyTagDefinition.php @@ -21,7 +21,7 @@ */ final class PhanWriteOnlyTagDefinition extends TagDefinition { - public const string NAME = 'phan-write-only'; + public const NAME = 'phan-write-only'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhpStanIgnoreLineTag/PhpStanIgnoreLineTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhpStanIgnoreLineTag/PhpStanIgnoreLineTagDefinition.php index c5af3c7..cd861db 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhpStanIgnoreLineTag/PhpStanIgnoreLineTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhpStanIgnoreLineTag/PhpStanIgnoreLineTagDefinition.php @@ -21,7 +21,7 @@ */ final class PhpStanIgnoreLineTagDefinition extends TagDefinition { - public const string NAME = 'phpstan-ignore-line'; + public const NAME = 'phpstan-ignore-line'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhpStanIgnoreNextLineTag/PhpStanIgnoreNextLineTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhpStanIgnoreNextLineTag/PhpStanIgnoreNextLineTagDefinition.php index d150313..c6f1643 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhpStanIgnoreNextLineTag/PhpStanIgnoreNextLineTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhpStanIgnoreNextLineTag/PhpStanIgnoreNextLineTagDefinition.php @@ -21,7 +21,7 @@ */ final class PhpStanIgnoreNextLineTagDefinition extends TagDefinition { - public const string NAME = 'phpstan-ignore-next-line'; + public const NAME = 'phpstan-ignore-next-line'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhpStanIgnoreTag/PhpStanIgnoreTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhpStanIgnoreTag/PhpStanIgnoreTagDefinition.php index 00441a8..bf8584c 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhpStanIgnoreTag/PhpStanIgnoreTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhpStanIgnoreTag/PhpStanIgnoreTagDefinition.php @@ -22,7 +22,7 @@ */ final class PhpStanIgnoreTagDefinition extends TagDefinition { - public const string NAME = 'phpstan-ignore'; + public const NAME = 'phpstan-ignore'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhpStanImpureTag/PhpStanImpureTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhpStanImpureTag/PhpStanImpureTagDefinition.php index e1606a8..6c23088 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhpStanImpureTag/PhpStanImpureTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhpStanImpureTag/PhpStanImpureTagDefinition.php @@ -21,7 +21,7 @@ */ final class PhpStanImpureTagDefinition extends TagDefinition { - public const string NAME = 'phpstan-impure'; + public const NAME = 'phpstan-impure'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PhpcsSuppressTag/PhpcsSuppressTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PhpcsSuppressTag/PhpcsSuppressTagDefinition.php index aace17f..77fd03c 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PhpcsSuppressTag/PhpcsSuppressTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PhpcsSuppressTag/PhpcsSuppressTagDefinition.php @@ -22,7 +22,7 @@ */ final class PhpcsSuppressTagDefinition extends TagDefinition { - public const string NAME = 'phpcsSuppress'; + public const NAME = 'phpcsSuppress'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PropertyTag/PropertyReadTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PropertyTag/PropertyReadTagDefinition.php index 1cf0bb8..f0ccc8b 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PropertyTag/PropertyReadTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PropertyTag/PropertyReadTagDefinition.php @@ -16,7 +16,7 @@ */ final class PropertyReadTagDefinition extends MagicPropertyTagDefinition { - public const string NAME = 'property-read'; + public const NAME = 'property-read'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PropertyTag/PropertyTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PropertyTag/PropertyTagDefinition.php index 7882269..0a9d315 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PropertyTag/PropertyTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PropertyTag/PropertyTagDefinition.php @@ -17,7 +17,7 @@ */ final class PropertyTagDefinition extends MagicPropertyTagDefinition { - public const string NAME = 'property'; + public const NAME = 'property'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PropertyTag/PropertyWriteTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PropertyTag/PropertyWriteTagDefinition.php index 1aba310..222cbdb 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PropertyTag/PropertyWriteTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PropertyTag/PropertyWriteTagDefinition.php @@ -17,7 +17,7 @@ */ final class PropertyWriteTagDefinition extends MagicPropertyTagDefinition { - public const string NAME = 'property-write'; + public const NAME = 'property-write'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmAssertUntaintedTag/PsalmAssertUntaintedTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmAssertUntaintedTag/PsalmAssertUntaintedTagDefinition.php index 8937731..fa18884 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmAssertUntaintedTag/PsalmAssertUntaintedTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmAssertUntaintedTag/PsalmAssertUntaintedTagDefinition.php @@ -22,7 +22,7 @@ */ final class PsalmAssertUntaintedTagDefinition extends TagDefinition { - public const string NAME = 'psalm-assert-untainted'; + public const NAME = 'psalm-assert-untainted'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmConsistentTemplatesTag/PsalmConsistentTemplatesTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmConsistentTemplatesTag/PsalmConsistentTemplatesTagDefinition.php index 0a7f6fc..073c954 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmConsistentTemplatesTag/PsalmConsistentTemplatesTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmConsistentTemplatesTag/PsalmConsistentTemplatesTagDefinition.php @@ -21,7 +21,7 @@ */ final class PsalmConsistentTemplatesTagDefinition extends TagDefinition { - public const string NAME = 'psalm-consistent-templates'; + public const NAME = 'psalm-consistent-templates'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmExternalMutationFreeTag/PsalmExternalMutationFreeTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmExternalMutationFreeTag/PsalmExternalMutationFreeTagDefinition.php index 36880b6..39087a9 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmExternalMutationFreeTag/PsalmExternalMutationFreeTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmExternalMutationFreeTag/PsalmExternalMutationFreeTagDefinition.php @@ -21,7 +21,7 @@ */ final class PsalmExternalMutationFreeTagDefinition extends TagDefinition { - public const string NAME = 'psalm-external-mutation-free'; + public const NAME = 'psalm-external-mutation-free'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmFlowTag/PsalmFlowTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmFlowTag/PsalmFlowTagDefinition.php index cb952ea..1ec95f0 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmFlowTag/PsalmFlowTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmFlowTag/PsalmFlowTagDefinition.php @@ -22,7 +22,7 @@ */ final class PsalmFlowTagDefinition extends TagDefinition { - public const string NAME = 'psalm-flow'; + public const NAME = 'psalm-flow'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmIfThisIsTag/PsalmIfThisIsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmIfThisIsTag/PsalmIfThisIsTagDefinition.php index f344eb0..363b4e8 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmIfThisIsTag/PsalmIfThisIsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmIfThisIsTag/PsalmIfThisIsTagDefinition.php @@ -23,7 +23,7 @@ */ final class PsalmIfThisIsTagDefinition extends TagDefinition { - public const string NAME = 'psalm-if-this-is'; + public const NAME = 'psalm-if-this-is'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreFalsableReturnTag/PsalmIgnoreFalsableReturnTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreFalsableReturnTag/PsalmIgnoreFalsableReturnTagDefinition.php index 22eff7a..c3dc99a 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreFalsableReturnTag/PsalmIgnoreFalsableReturnTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreFalsableReturnTag/PsalmIgnoreFalsableReturnTagDefinition.php @@ -21,7 +21,7 @@ */ final class PsalmIgnoreFalsableReturnTagDefinition extends TagDefinition { - public const string NAME = 'psalm-ignore-falsable-return'; + public const NAME = 'psalm-ignore-falsable-return'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreNullableReturnTag/PsalmIgnoreNullableReturnTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreNullableReturnTag/PsalmIgnoreNullableReturnTagDefinition.php index 561675d..d1e92a6 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreNullableReturnTag/PsalmIgnoreNullableReturnTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreNullableReturnTag/PsalmIgnoreNullableReturnTagDefinition.php @@ -21,7 +21,7 @@ */ final class PsalmIgnoreNullableReturnTagDefinition extends TagDefinition { - public const string NAME = 'psalm-ignore-nullable-return'; + public const NAME = 'psalm-ignore-nullable-return'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreVarTag/PsalmIgnoreVarTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreVarTag/PsalmIgnoreVarTagDefinition.php index 484db1a..c23ad07 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreVarTag/PsalmIgnoreVarTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreVarTag/PsalmIgnoreVarTagDefinition.php @@ -22,7 +22,7 @@ */ final class PsalmIgnoreVarTagDefinition extends TagDefinition { - public const string NAME = 'psalm-ignore-var'; + public const NAME = 'psalm-ignore-var'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreVariableMethodTag/PsalmIgnoreVariableMethodTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreVariableMethodTag/PsalmIgnoreVariableMethodTagDefinition.php index 70cd375..3274744 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreVariableMethodTag/PsalmIgnoreVariableMethodTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreVariableMethodTag/PsalmIgnoreVariableMethodTagDefinition.php @@ -21,7 +21,7 @@ */ final class PsalmIgnoreVariableMethodTagDefinition extends TagDefinition { - public const string NAME = 'psalm-ignore-variable-method'; + public const NAME = 'psalm-ignore-variable-method'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreVariablePropertyTag/PsalmIgnoreVariablePropertyTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreVariablePropertyTag/PsalmIgnoreVariablePropertyTagDefinition.php index e23fd15..b1054e8 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreVariablePropertyTag/PsalmIgnoreVariablePropertyTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmIgnoreVariablePropertyTag/PsalmIgnoreVariablePropertyTagDefinition.php @@ -21,7 +21,7 @@ */ final class PsalmIgnoreVariablePropertyTagDefinition extends TagDefinition { - public const string NAME = 'psalm-ignore-variable-property'; + public const NAME = 'psalm-ignore-variable-property'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmInheritorsTag/PsalmInheritorsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmInheritorsTag/PsalmInheritorsTagDefinition.php index a5584c0..c7ea26c 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmInheritorsTag/PsalmInheritorsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmInheritorsTag/PsalmInheritorsTagDefinition.php @@ -23,7 +23,7 @@ */ final class PsalmInheritorsTagDefinition extends TagDefinition { - public const string NAME = 'psalm-inheritors'; + public const NAME = 'psalm-inheritors'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmMutationFreeTag/PsalmMutationFreeTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmMutationFreeTag/PsalmMutationFreeTagDefinition.php index 9553454..21d3950 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmMutationFreeTag/PsalmMutationFreeTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmMutationFreeTag/PsalmMutationFreeTagDefinition.php @@ -22,7 +22,7 @@ */ final class PsalmMutationFreeTagDefinition extends TagDefinition { - public const string NAME = 'psalm-mutation-free'; + public const NAME = 'psalm-mutation-free'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmNoSealMethodsTag/PsalmNoSealMethodsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmNoSealMethodsTag/PsalmNoSealMethodsTagDefinition.php index d68c2ac..bfcc2bf 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmNoSealMethodsTag/PsalmNoSealMethodsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmNoSealMethodsTag/PsalmNoSealMethodsTagDefinition.php @@ -21,7 +21,7 @@ */ final class PsalmNoSealMethodsTagDefinition extends TagDefinition { - public const string NAME = 'psalm-no-seal-methods'; + public const NAME = 'psalm-no-seal-methods'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmNoSealPropertiesTag/PsalmNoSealPropertiesTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmNoSealPropertiesTag/PsalmNoSealPropertiesTagDefinition.php index a946906..fc4ccba 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmNoSealPropertiesTag/PsalmNoSealPropertiesTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmNoSealPropertiesTag/PsalmNoSealPropertiesTagDefinition.php @@ -21,7 +21,7 @@ */ final class PsalmNoSealPropertiesTagDefinition extends TagDefinition { - public const string NAME = 'psalm-no-seal-properties'; + public const NAME = 'psalm-no-seal-properties'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmOverrideMethodVisibilityTag/PsalmOverrideMethodVisibilityTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmOverrideMethodVisibilityTag/PsalmOverrideMethodVisibilityTagDefinition.php index 351cc6f..79775b0 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmOverrideMethodVisibilityTag/PsalmOverrideMethodVisibilityTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmOverrideMethodVisibilityTag/PsalmOverrideMethodVisibilityTagDefinition.php @@ -22,7 +22,7 @@ */ final class PsalmOverrideMethodVisibilityTagDefinition extends TagDefinition { - public const string NAME = 'psalm-override-method-visibility'; + public const NAME = 'psalm-override-method-visibility'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmOverridePropertyVisibilityTag/PsalmOverridePropertyVisibilityTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmOverridePropertyVisibilityTag/PsalmOverridePropertyVisibilityTagDefinition.php index 225eaff..ff52009 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmOverridePropertyVisibilityTag/PsalmOverridePropertyVisibilityTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmOverridePropertyVisibilityTag/PsalmOverridePropertyVisibilityTagDefinition.php @@ -22,7 +22,7 @@ */ final class PsalmOverridePropertyVisibilityTagDefinition extends TagDefinition { - public const string NAME = 'psalm-override-property-visibility'; + public const NAME = 'psalm-override-property-visibility'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmScopeThisTag/PsalmScopeThisTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmScopeThisTag/PsalmScopeThisTagDefinition.php index 369f048..ee5d871 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmScopeThisTag/PsalmScopeThisTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmScopeThisTag/PsalmScopeThisTagDefinition.php @@ -24,7 +24,7 @@ */ final class PsalmScopeThisTagDefinition extends TagDefinition { - public const string NAME = 'psalm-scope-this'; + public const NAME = 'psalm-scope-this'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmStubOverrideTag/PsalmStubOverrideTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmStubOverrideTag/PsalmStubOverrideTagDefinition.php index 56ecc02..6265e48 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmStubOverrideTag/PsalmStubOverrideTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmStubOverrideTag/PsalmStubOverrideTagDefinition.php @@ -21,7 +21,7 @@ */ final class PsalmStubOverrideTagDefinition extends TagDefinition { - public const string NAME = 'psalm-stub-override'; + public const NAME = 'psalm-stub-override'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmTaintEscapeTag/PsalmTaintEscapeTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmTaintEscapeTag/PsalmTaintEscapeTagDefinition.php index 875ab70..40b5e05 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmTaintEscapeTag/PsalmTaintEscapeTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmTaintEscapeTag/PsalmTaintEscapeTagDefinition.php @@ -22,7 +22,7 @@ */ final class PsalmTaintEscapeTagDefinition extends TagDefinition { - public const string NAME = 'psalm-taint-escape'; + public const NAME = 'psalm-taint-escape'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmTaintSinkTag/PsalmTaintSinkTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmTaintSinkTag/PsalmTaintSinkTagDefinition.php index 7773e8c..de3ddc1 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmTaintSinkTag/PsalmTaintSinkTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmTaintSinkTag/PsalmTaintSinkTagDefinition.php @@ -21,7 +21,7 @@ */ final class PsalmTaintSinkTagDefinition extends TagDefinition { - public const string NAME = 'psalm-taint-sink'; + public const NAME = 'psalm-taint-sink'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmTaintSourceTag/PsalmTaintSourceTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmTaintSourceTag/PsalmTaintSourceTagDefinition.php index a8f1786..11c6b09 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmTaintSourceTag/PsalmTaintSourceTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmTaintSourceTag/PsalmTaintSourceTagDefinition.php @@ -22,7 +22,7 @@ */ final class PsalmTaintSourceTagDefinition extends TagDefinition { - public const string NAME = 'psalm-taint-source'; + public const NAME = 'psalm-taint-source'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmTaintSpecializeTag/PsalmTaintSpecializeTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmTaintSpecializeTag/PsalmTaintSpecializeTagDefinition.php index 6035945..e388169 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmTaintSpecializeTag/PsalmTaintSpecializeTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmTaintSpecializeTag/PsalmTaintSpecializeTagDefinition.php @@ -22,7 +22,7 @@ */ final class PsalmTaintSpecializeTagDefinition extends TagDefinition { - public const string NAME = 'psalm-taint-specialize'; + public const NAME = 'psalm-taint-specialize'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmTaintUnescapeTag/PsalmTaintUnescapeTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmTaintUnescapeTag/PsalmTaintUnescapeTagDefinition.php index b79bd9e..512dde1 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmTaintUnescapeTag/PsalmTaintUnescapeTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmTaintUnescapeTag/PsalmTaintUnescapeTagDefinition.php @@ -23,7 +23,7 @@ */ final class PsalmTaintUnescapeTagDefinition extends TagDefinition { - public const string NAME = 'psalm-taint-unescape'; + public const NAME = 'psalm-taint-unescape'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmTraceTag/PsalmTraceTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmTraceTag/PsalmTraceTagDefinition.php index 8e10a62..74fe186 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmTraceTag/PsalmTraceTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmTraceTag/PsalmTraceTagDefinition.php @@ -22,7 +22,7 @@ */ final class PsalmTraceTagDefinition extends TagDefinition { - public const string NAME = 'psalm-trace'; + public const NAME = 'psalm-trace'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PsalmVariadicTag/PsalmVariadicTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PsalmVariadicTag/PsalmVariadicTagDefinition.php index dfe234a..2f5ebd3 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PsalmVariadicTag/PsalmVariadicTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PsalmVariadicTag/PsalmVariadicTagDefinition.php @@ -21,7 +21,7 @@ */ final class PsalmVariadicTagDefinition extends TagDefinition { - public const string NAME = 'psalm-variadic'; + public const NAME = 'psalm-variadic'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PureTag/PureTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PureTag/PureTagDefinition.php index 37dc3fb..85ac240 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PureTag/PureTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PureTag/PureTagDefinition.php @@ -22,7 +22,7 @@ */ final class PureTagDefinition extends TagDefinition { - public const string NAME = 'pure'; + public const NAME = 'pure'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/PureUnlessCallableIsImpureTag/PureUnlessCallableIsImpureTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/PureUnlessCallableIsImpureTag/PureUnlessCallableIsImpureTagDefinition.php index 60910a0..b32df9a 100644 --- a/libs/phpdoc/src/DocBlock/Tag/PureUnlessCallableIsImpureTag/PureUnlessCallableIsImpureTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/PureUnlessCallableIsImpureTag/PureUnlessCallableIsImpureTagDefinition.php @@ -21,7 +21,7 @@ */ final class PureUnlessCallableIsImpureTagDefinition extends TagDefinition { - public const string NAME = 'pure-unless-callable-is-impure'; + public const NAME = 'pure-unless-callable-is-impure'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ReadonlyAllowPrivateMutationTag/ReadonlyAllowPrivateMutationTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ReadonlyAllowPrivateMutationTag/ReadonlyAllowPrivateMutationTagDefinition.php index 388b6d6..8a1eb86 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ReadonlyAllowPrivateMutationTag/ReadonlyAllowPrivateMutationTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ReadonlyAllowPrivateMutationTag/ReadonlyAllowPrivateMutationTagDefinition.php @@ -22,7 +22,7 @@ */ final class ReadonlyAllowPrivateMutationTagDefinition extends TagDefinition { - public const string NAME = 'readonly-allow-private-mutation'; + public const NAME = 'readonly-allow-private-mutation'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ReadonlyTag/ReadonlyTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ReadonlyTag/ReadonlyTagDefinition.php index 41a3255..6ce91fb 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ReadonlyTag/ReadonlyTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ReadonlyTag/ReadonlyTagDefinition.php @@ -21,7 +21,7 @@ */ final class ReadonlyTagDefinition extends TagDefinition { - public const string NAME = 'readonly'; + public const NAME = 'readonly'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ReferencedTagInterface.php b/libs/phpdoc/src/DocBlock/Tag/ReferencedTagInterface.php index a947398..0f7b5ce 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ReferencedTagInterface.php +++ b/libs/phpdoc/src/DocBlock/Tag/ReferencedTagInterface.php @@ -10,15 +10,7 @@ * A tag that points to another element or an external resource. * * @template-covariant TReference of ReferenceInterface = ReferenceInterface + * + * @property-read ReferenceInterface $reference The reference the tag points to. */ -interface ReferencedTagInterface extends TagInterface -{ - /** - * The target the tag points to. - * - * @var TReference - */ - public ReferenceInterface $reference { - get; - } -} +interface ReferencedTagInterface extends TagInterface {} diff --git a/libs/phpdoc/src/DocBlock/Tag/RequireInheritanceTag/RequireExtendsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/RequireInheritanceTag/RequireExtendsTagDefinition.php index 0347f7f..275f2fb 100644 --- a/libs/phpdoc/src/DocBlock/Tag/RequireInheritanceTag/RequireExtendsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/RequireInheritanceTag/RequireExtendsTagDefinition.php @@ -17,7 +17,7 @@ */ final class RequireExtendsTagDefinition extends RequireInheritanceTagDefinition { - public const string NAME = 'require-extends'; + public const NAME = 'require-extends'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/RequireInheritanceTag/RequireImplementsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/RequireInheritanceTag/RequireImplementsTagDefinition.php index ec58c89..e3e1ab1 100644 --- a/libs/phpdoc/src/DocBlock/Tag/RequireInheritanceTag/RequireImplementsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/RequireInheritanceTag/RequireImplementsTagDefinition.php @@ -17,7 +17,7 @@ */ final class RequireImplementsTagDefinition extends RequireInheritanceTagDefinition { - public const string NAME = 'require-implements'; + public const NAME = 'require-implements'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ReturnTag/ReturnTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ReturnTag/ReturnTagDefinition.php index 6531469..b07ad9e 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ReturnTag/ReturnTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ReturnTag/ReturnTagDefinition.php @@ -26,7 +26,7 @@ */ final class ReturnTagDefinition extends TagDefinition { - public const string NAME = 'return'; + public const NAME = 'return'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/SealMethodsTag/SealMethodsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/SealMethodsTag/SealMethodsTagDefinition.php index c7ed113..2059ae1 100644 --- a/libs/phpdoc/src/DocBlock/Tag/SealMethodsTag/SealMethodsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/SealMethodsTag/SealMethodsTagDefinition.php @@ -21,7 +21,7 @@ */ final class SealMethodsTagDefinition extends TagDefinition { - public const string NAME = 'seal-methods'; + public const NAME = 'seal-methods'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/SealPropertiesTag/SealPropertiesTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/SealPropertiesTag/SealPropertiesTagDefinition.php index a8d797c..46786be 100644 --- a/libs/phpdoc/src/DocBlock/Tag/SealPropertiesTag/SealPropertiesTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/SealPropertiesTag/SealPropertiesTagDefinition.php @@ -21,7 +21,7 @@ */ final class SealPropertiesTagDefinition extends TagDefinition { - public const string NAME = 'seal-properties'; + public const NAME = 'seal-properties'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/SeeTag/SeeTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/SeeTag/SeeTagDefinition.php index 81ef1d9..e98154b 100644 --- a/libs/phpdoc/src/DocBlock/Tag/SeeTag/SeeTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/SeeTag/SeeTagDefinition.php @@ -37,7 +37,7 @@ */ final class SeeTagDefinition extends TagDefinition { - public const string NAME = 'see'; + public const NAME = 'see'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/SelfOutTag/SelfOutTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/SelfOutTag/SelfOutTagDefinition.php index 2ce9d78..eeaff05 100644 --- a/libs/phpdoc/src/DocBlock/Tag/SelfOutTag/SelfOutTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/SelfOutTag/SelfOutTagDefinition.php @@ -24,7 +24,7 @@ */ final class SelfOutTagDefinition extends TagDefinition { - public const string NAME = 'self-out'; + public const NAME = 'self-out'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/SinceTag/SinceTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/SinceTag/SinceTagDefinition.php index e17d06a..f6caae5 100644 --- a/libs/phpdoc/src/DocBlock/Tag/SinceTag/SinceTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/SinceTag/SinceTagDefinition.php @@ -22,7 +22,7 @@ */ final class SinceTagDefinition extends TagDefinition { - public const string NAME = 'since'; + public const NAME = 'since'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/SourceTag/SourceTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/SourceTag/SourceTagDefinition.php index 01559b5..829d85c 100644 --- a/libs/phpdoc/src/DocBlock/Tag/SourceTag/SourceTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/SourceTag/SourceTagDefinition.php @@ -22,7 +22,7 @@ */ final class SourceTagDefinition extends TagDefinition { - public const string NAME = 'source'; + public const NAME = 'source'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/StaticTag/StaticTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/StaticTag/StaticTagDefinition.php index 4030a94..bc0005d 100644 --- a/libs/phpdoc/src/DocBlock/Tag/StaticTag/StaticTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/StaticTag/StaticTagDefinition.php @@ -20,7 +20,7 @@ */ final class StaticTagDefinition extends TagDefinition { - public const string NAME = 'static'; + public const NAME = 'static'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/StaticVarTag/StaticVarTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/StaticVarTag/StaticVarTagDefinition.php index 57a69c8..d8655f9 100644 --- a/libs/phpdoc/src/DocBlock/Tag/StaticVarTag/StaticVarTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/StaticVarTag/StaticVarTagDefinition.php @@ -24,7 +24,7 @@ */ final class StaticVarTagDefinition extends TagDefinition { - public const string NAME = 'staticvar'; + public const NAME = 'staticvar'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/SubpackageTag/SubpackageTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/SubpackageTag/SubpackageTagDefinition.php index 30d0672..f2d313a 100644 --- a/libs/phpdoc/src/DocBlock/Tag/SubpackageTag/SubpackageTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/SubpackageTag/SubpackageTagDefinition.php @@ -21,7 +21,7 @@ */ final class SubpackageTagDefinition extends TagDefinition { - public const string NAME = 'subpackage'; + public const NAME = 'subpackage'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/SuppressTag/SuppressTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/SuppressTag/SuppressTagDefinition.php index aa48547..983ccfe 100644 --- a/libs/phpdoc/src/DocBlock/Tag/SuppressTag/SuppressTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/SuppressTag/SuppressTagDefinition.php @@ -22,7 +22,7 @@ */ final class SuppressTagDefinition extends TagDefinition { - public const string NAME = 'suppress'; + public const NAME = 'suppress'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/TagInterface.php b/libs/phpdoc/src/DocBlock/Tag/TagInterface.php index 6a37d71..c404f3e 100644 --- a/libs/phpdoc/src/DocBlock/Tag/TagInterface.php +++ b/libs/phpdoc/src/DocBlock/Tag/TagInterface.php @@ -9,25 +9,9 @@ /** * Representation of the phpdoc tag. + * + * @property-read non-empty-string $name Gets tag name string without the "@" prefix. + * @property-read ?DescriptionInterface $description Gets an optional description object or {@see null} in case of + * description is not defined in the entry. */ -interface TagInterface extends ComponentInterface -{ - /** - * Gets tag name string without the '@' prefix. - * - * That is, can contain all the characters that can match the PHP FQN - * (Fully Qualified Name), as well as the '-' character. - */ - public string $name { - get; - } - - /** - * Gets an optional description object which can be represented as - * a {@see string} and contains additional information or {@see null} - * in case of description is not defined in the entry. - */ - public ?DescriptionInterface $description { - get; - } -} +interface TagInterface extends ComponentInterface {} diff --git a/libs/phpdoc/src/DocBlock/Tag/TemplateTag/TemplateContravariantTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/TemplateTag/TemplateContravariantTagDefinition.php index ecacc8a..0783371 100644 --- a/libs/phpdoc/src/DocBlock/Tag/TemplateTag/TemplateContravariantTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/TemplateTag/TemplateContravariantTagDefinition.php @@ -17,7 +17,7 @@ */ final class TemplateContravariantTagDefinition extends TypeParameterTagDefinition { - public const string NAME = 'template-contravariant'; + public const NAME = 'template-contravariant'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/TemplateTag/TemplateCovariantTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/TemplateTag/TemplateCovariantTagDefinition.php index d32bbfb..b7a2100 100644 --- a/libs/phpdoc/src/DocBlock/Tag/TemplateTag/TemplateCovariantTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/TemplateTag/TemplateCovariantTagDefinition.php @@ -16,7 +16,7 @@ */ final class TemplateCovariantTagDefinition extends TypeParameterTagDefinition { - public const string NAME = 'template-covariant'; + public const NAME = 'template-covariant'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/TemplateTag/TemplateTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/TemplateTag/TemplateTagDefinition.php index 855e9e0..1cde344 100644 --- a/libs/phpdoc/src/DocBlock/Tag/TemplateTag/TemplateTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/TemplateTag/TemplateTagDefinition.php @@ -16,7 +16,7 @@ */ final class TemplateTagDefinition extends TypeParameterTagDefinition { - public const string NAME = 'template'; + public const NAME = 'template'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/ThrowsTag/ThrowsTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/ThrowsTag/ThrowsTagDefinition.php index 864be59..75e78b5 100644 --- a/libs/phpdoc/src/DocBlock/Tag/ThrowsTag/ThrowsTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/ThrowsTag/ThrowsTagDefinition.php @@ -26,7 +26,7 @@ */ final class ThrowsTagDefinition extends TagDefinition { - public const string NAME = 'throws'; + public const NAME = 'throws'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/TodoTag/TodoTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/TodoTag/TodoTagDefinition.php index e7285fd..4d22eba 100644 --- a/libs/phpdoc/src/DocBlock/Tag/TodoTag/TodoTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/TodoTag/TodoTagDefinition.php @@ -20,7 +20,7 @@ */ final class TodoTagDefinition extends TagDefinition { - public const string NAME = 'todo'; + public const NAME = 'todo'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/TypeAliasTag/TypeAliasTag.php b/libs/phpdoc/src/DocBlock/Tag/TypeAliasTag/TypeAliasTag.php index 68fa19d..0fb247d 100644 --- a/libs/phpdoc/src/DocBlock/Tag/TypeAliasTag/TypeAliasTag.php +++ b/libs/phpdoc/src/DocBlock/Tag/TypeAliasTag/TypeAliasTag.php @@ -13,9 +13,10 @@ */ final class TypeAliasTag extends Tag { - public TypeNode $type { - get => $this->statement->type; - } + /** + * The type declared by the tag. + */ + public readonly TypeNode $type; public function __construct( string $name, @@ -30,6 +31,8 @@ public function __construct( */ protected readonly TypeReference $statement, ) { + $this->type = $statement->type; + parent::__construct($name); } diff --git a/libs/phpdoc/src/DocBlock/Tag/TypeAliasTag/TypeAliasTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/TypeAliasTag/TypeAliasTagDefinition.php index e068398..2b4f9a7 100644 --- a/libs/phpdoc/src/DocBlock/Tag/TypeAliasTag/TypeAliasTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/TypeAliasTag/TypeAliasTagDefinition.php @@ -22,7 +22,7 @@ */ final class TypeAliasTagDefinition extends TagDefinition { - public const string NAME = 'type'; + public const NAME = 'type'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/TypedTag.php b/libs/phpdoc/src/DocBlock/Tag/TypedTag.php index 5a87a04..1e7d54a 100644 --- a/libs/phpdoc/src/DocBlock/Tag/TypedTag.php +++ b/libs/phpdoc/src/DocBlock/Tag/TypedTag.php @@ -13,9 +13,10 @@ */ abstract class TypedTag extends Tag implements TypedTagInterface { - public TypeNode $type { - get => $this->statement->type; - } + /** + * The type declared by the tag. + */ + public readonly TypeNode $type; public function __construct( string $name, @@ -25,6 +26,8 @@ public function __construct( protected readonly TypeReference $statement, ?DescriptionInterface $description = null, ) { + $this->type = $statement->type; + parent::__construct($name, $description); } diff --git a/libs/phpdoc/src/DocBlock/Tag/TypedTagInterface.php b/libs/phpdoc/src/DocBlock/Tag/TypedTagInterface.php index e50efef..9533afa 100644 --- a/libs/phpdoc/src/DocBlock/Tag/TypedTagInterface.php +++ b/libs/phpdoc/src/DocBlock/Tag/TypedTagInterface.php @@ -8,13 +8,7 @@ /** * A tag that carries a single type. + * + * @property-read TypeNode $type The type declared by the tag. */ -interface TypedTagInterface extends TagInterface -{ - /** - * The type declared by the tag. - */ - public TypeNode $type { - get; - } -} +interface TypedTagInterface extends TagInterface {} diff --git a/libs/phpdoc/src/DocBlock/Tag/UnusedParamTag/UnusedParamTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/UnusedParamTag/UnusedParamTagDefinition.php index f695910..5076f08 100644 --- a/libs/phpdoc/src/DocBlock/Tag/UnusedParamTag/UnusedParamTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/UnusedParamTag/UnusedParamTagDefinition.php @@ -22,7 +22,7 @@ */ final class UnusedParamTagDefinition extends TagDefinition { - public const string NAME = 'unused-param'; + public const NAME = 'unused-param'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/UsedByTag/UsedByTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/UsedByTag/UsedByTagDefinition.php index dd0d44b..2dab863 100644 --- a/libs/phpdoc/src/DocBlock/Tag/UsedByTag/UsedByTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/UsedByTag/UsedByTagDefinition.php @@ -23,7 +23,7 @@ */ final class UsedByTagDefinition extends TagDefinition { - public const string NAME = 'used-by'; + public const NAME = 'used-by'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/UsesTag/UsesTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/UsesTag/UsesTagDefinition.php index 5070543..24159e0 100644 --- a/libs/phpdoc/src/DocBlock/Tag/UsesTag/UsesTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/UsesTag/UsesTagDefinition.php @@ -23,7 +23,7 @@ */ final class UsesTagDefinition extends TagDefinition { - public const string NAME = 'uses'; + public const NAME = 'uses'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/VarTag/VarTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/VarTag/VarTagDefinition.php index a7ba1b5..4a921a2 100644 --- a/libs/phpdoc/src/DocBlock/Tag/VarTag/VarTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/VarTag/VarTagDefinition.php @@ -24,7 +24,7 @@ */ final class VarTagDefinition extends TagDefinition { - public const string NAME = 'var'; + public const NAME = 'var'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/VariableTagInterface.php b/libs/phpdoc/src/DocBlock/Tag/VariableTagInterface.php index c948a82..c45bca0 100644 --- a/libs/phpdoc/src/DocBlock/Tag/VariableTagInterface.php +++ b/libs/phpdoc/src/DocBlock/Tag/VariableTagInterface.php @@ -6,15 +6,7 @@ /** * A tag that refers to a variable. + * + * @property-read non-empty-string $variable The referenced variable name, without the leading "$". */ -interface VariableTagInterface extends TagInterface -{ - /** - * The referenced variable name, without the leading "$". - * - * @var non-empty-string - */ - public string $variable { - get; - } -} +interface VariableTagInterface extends TagInterface {} diff --git a/libs/phpdoc/src/DocBlock/Tag/VersionTag/VersionTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/VersionTag/VersionTagDefinition.php index dab4fed..379d9d4 100644 --- a/libs/phpdoc/src/DocBlock/Tag/VersionTag/VersionTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/VersionTag/VersionTagDefinition.php @@ -21,7 +21,7 @@ */ final class VersionTagDefinition extends TagDefinition { - public const string NAME = 'version'; + public const NAME = 'version'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/Tag/VersionedTagInterface.php b/libs/phpdoc/src/DocBlock/Tag/VersionedTagInterface.php index 8d40141..76bd9b3 100644 --- a/libs/phpdoc/src/DocBlock/Tag/VersionedTagInterface.php +++ b/libs/phpdoc/src/DocBlock/Tag/VersionedTagInterface.php @@ -6,15 +6,7 @@ /** * A tag that carries an optional version. + * + * @property-read ?non-empty-string $version The version the tag refers to, if any. */ -interface VersionedTagInterface extends TagInterface -{ - /** - * The version the tag refers to, or {@see null} when none was given. - * - * @var non-empty-string|null - */ - public ?string $version { - get; - } -} +interface VersionedTagInterface extends TagInterface {} diff --git a/libs/phpdoc/src/DocBlock/Tag/YieldTag/YieldTagDefinition.php b/libs/phpdoc/src/DocBlock/Tag/YieldTag/YieldTagDefinition.php index 870fc9c..4c7dda2 100644 --- a/libs/phpdoc/src/DocBlock/Tag/YieldTag/YieldTagDefinition.php +++ b/libs/phpdoc/src/DocBlock/Tag/YieldTag/YieldTagDefinition.php @@ -23,7 +23,7 @@ */ final class YieldTagDefinition extends TagDefinition { - public const string NAME = 'yield'; + public const NAME = 'yield'; public function __construct() { diff --git a/libs/phpdoc/src/DocBlock/TagDefinition/Spec.php b/libs/phpdoc/src/DocBlock/TagDefinition/Spec.php index 9a45413..e57adba 100644 --- a/libs/phpdoc/src/DocBlock/TagDefinition/Spec.php +++ b/libs/phpdoc/src/DocBlock/TagDefinition/Spec.php @@ -12,7 +12,7 @@ use TypeLang\PhpDoc\Parser\Grammar\Rule\RuleInterface; use TypeLang\PhpDoc\Parser\Grammar\Rule\SequencingRule; -final readonly class Spec +final class Spec { /** * @param non-empty-string $value diff --git a/libs/phpdoc/src/DocBlock/TagDefinition/TagDefinitionInterface.php b/libs/phpdoc/src/DocBlock/TagDefinition/TagDefinitionInterface.php index 5d1d654..760d165 100644 --- a/libs/phpdoc/src/DocBlock/TagDefinition/TagDefinitionInterface.php +++ b/libs/phpdoc/src/DocBlock/TagDefinition/TagDefinitionInterface.php @@ -12,37 +12,13 @@ * * The shape of its body (a {@see RuleInterface}) and how to build a {@see TagInterface} * from the parsed pieces. + * + * @property-read non-empty-string $name Canonical tag name. + * @property-read RuleInterface $spec Tag definition specification: shape of the tag body. + * @property-read TagPlacement $placement Where the tag may appear: only inline, only as a block tag, or anywhere. */ interface TagDefinitionInterface extends \Stringable { - /** - * Canonical tag name - * - * @var non-empty-string - */ - public string $name { - get; - } - - /** - * Tag definition specification: shape of the tag body. - */ - public RuleInterface $spec { - get; - } - - /** - * Where the tag may appear: only inline (inside a description as a - * "{@tag}" sequence), only as a block tag, or anywhere. - * - * A block-only tag (such as "@param") is never lifted out of a description: - * a "{@param}" written in running text stays raw text instead of being - * parsed as an inline tag. - */ - public TagPlacement $placement { - get; - } - /** * Builds the tag from the values captured while matching {@see $spec}. * diff --git a/libs/phpdoc/src/DocBlock/TagDefinition/TagPayload.php b/libs/phpdoc/src/DocBlock/TagDefinition/TagPayload.php index a03a747..dc7eda2 100644 --- a/libs/phpdoc/src/DocBlock/TagDefinition/TagPayload.php +++ b/libs/phpdoc/src/DocBlock/TagDefinition/TagPayload.php @@ -16,7 +16,7 @@ * $result->getAll('parameter'); // every value captured under the alias * ``` */ -final readonly class TagPayload +final class TagPayload { public function __construct( /** @@ -24,7 +24,7 @@ public function __construct( * * @var array> */ - private array $captures = [], + private readonly array $captures = [], ) {} public function has(string $alias): bool diff --git a/libs/phpdoc/src/DocBlock/TagDefinition/TagPlacement.php b/libs/phpdoc/src/DocBlock/TagDefinition/TagPlacement.php index 62c8f5f..fbbfa8d 100644 --- a/libs/phpdoc/src/DocBlock/TagDefinition/TagPlacement.php +++ b/libs/phpdoc/src/DocBlock/TagDefinition/TagPlacement.php @@ -27,5 +27,5 @@ enum TagPlacement /** * Placement assumed when a definition does not specify one. */ - public const self DEFAULT = self::Any; + public const DEFAULT = self::Any; } diff --git a/libs/phpdoc/src/DocBlockParser.php b/libs/phpdoc/src/DocBlockParser.php index 93a0dac..75b75b6 100644 --- a/libs/phpdoc/src/DocBlockParser.php +++ b/libs/phpdoc/src/DocBlockParser.php @@ -16,6 +16,7 @@ use TypeLang\PhpDoc\Parser\Description\DescriptionParserInterface; use TypeLang\PhpDoc\Parser\DocBlockAnalyzer; use TypeLang\PhpDoc\Parser\Grammar\CombinatorInterface; +use TypeLang\PhpDoc\Parser\Grammar\Cursor; use TypeLang\PhpDoc\Parser\Splitter\Segment; use TypeLang\PhpDoc\Parser\Splitter\SplitterInterface; use TypeLang\PhpDoc\Parser\Splitter\StringSplitter; @@ -35,15 +36,15 @@ /** * @phpstan-import-type CombinatorType from CombinatorInterface */ -final readonly class DocBlockParser implements DocBlockParserInterface +final class DocBlockParser implements DocBlockParserInterface { - public TagFactoryInterface $factory; + public readonly TagFactoryInterface $factory; - public TagRegistryInterface $tags; + public readonly TagRegistryInterface $tags; - private DocBlockAnalyzer $docBlockAnalyzer; - private TagParserInterface $tagParser; - private DescriptionParserInterface $descriptionParser; + private readonly DocBlockAnalyzer $docBlockAnalyzer; + private readonly TagParserInterface $tagParser; + private readonly DescriptionParserInterface $descriptionParser; /** * @param iterable $platforms additional tag platforms @@ -72,7 +73,9 @@ public static function createDefault(iterable $additionalPlatforms = []): self new PhanPlatform(), new PhpStormPlatform(), new PhpCodeSnifferPlatform(), - ...\iterator_to_array($additionalPlatforms, false), + ...(\is_array($additionalPlatforms) + ? \array_values($additionalPlatforms) + : \iterator_to_array($additionalPlatforms, false)), ]); } @@ -112,17 +115,18 @@ private function createTagRegistry(array $platforms): TagRegistryInterface $aliases = []; foreach ($platforms as $platform) { - foreach ($platform->tags as $name => $definition) { + foreach ($platform->getTags() as $name => $definition) { $definitions[$name] = $definition; } - foreach ($platform->aliases as $alias => $canonical) { + foreach ($platform->getAliases() as $alias => $canonical) { $aliases[$alias] = $canonical; } } - return new TagRegistryBuilder($definitions, $aliases) - ->build(); + $builder = new TagRegistryBuilder($definitions, $aliases); + + return $builder->build(); } /** @@ -134,16 +138,24 @@ private function createCombinators(array $platforms): array $combinators = []; foreach ($platforms as $platform) { - foreach ($platform->combinators as $name => $combinator) { + foreach ($platform->getCombinators() as $name => $combinator) { $combinators[$name] = $combinator; } } - // Description is always present and cannot be redefined - $combinators[DescriptionCombinator::NAME] = new \ReflectionClass(DescriptionCombinator::class) - ->newLazyProxy(fn(): DescriptionCombinator => new DescriptionCombinator( + $description = null; + + // Description is always present and cannot be redefined. + // + // The combinator is created on the first call, because the description + // parser is not available at the moment of the combinators creation. + $combinators[DescriptionCombinator::NAME] = function (Cursor $cursor) use (&$description): mixed { + $description ??= new DescriptionCombinator( descriptionParser: $this->descriptionParser, - )); + ); + + return $description($cursor); + }; return $combinators; } diff --git a/libs/phpdoc/src/Exception/ParsingExceptionInterface.php b/libs/phpdoc/src/Exception/ParsingExceptionInterface.php index 98399b2..e1313b7 100644 --- a/libs/phpdoc/src/Exception/ParsingExceptionInterface.php +++ b/libs/phpdoc/src/Exception/ParsingExceptionInterface.php @@ -6,22 +6,8 @@ /** * Error occurring while processing phpdoc content. + * + * @property-read string $source Gets the full docblock content in which the error occurred. + * @property-read int<0, max> $offset Gets the offset at which the error occurred. */ -interface ParsingExceptionInterface extends PhpDocExceptionInterface -{ - /** - * Gets the full docblock content in which the error occurred. - */ - public string $source { - get; - } - - /** - * Gets the byte offset at the location where the error occurs. - * - * @var int<0, max> - */ - public int $offset { - get; - } -} +interface ParsingExceptionInterface extends PhpDocExceptionInterface {} diff --git a/libs/phpdoc/src/Parser/Description/BalancedBraceAwareParser.php b/libs/phpdoc/src/Parser/Description/BalancedBraceAwareParser.php index d496897..b7b15a5 100644 --- a/libs/phpdoc/src/Parser/Description/BalancedBraceAwareParser.php +++ b/libs/phpdoc/src/Parser/Description/BalancedBraceAwareParser.php @@ -18,19 +18,19 @@ * * An inline tag is any `{@...}` sequence whose curly braces are balanced. */ -final readonly class BalancedBraceAwareParser implements DescriptionParserInterface +final class BalancedBraceAwareParser implements DescriptionParserInterface { - private const string INLINE_TAG_START_SEQUENCE = '{@'; + private const INLINE_TAG_START_SEQUENCE = '{@'; - private const string NESTING_INC_CHAR = '{'; - private const string NESTING_DEC_CHAR = '}'; + private const NESTING_INC_CHAR = '{'; + private const NESTING_DEC_CHAR = '}'; - private const string NESTING_CHARS + private const NESTING_CHARS = self::NESTING_INC_CHAR . self::NESTING_DEC_CHAR; public function __construct( - private TagParserInterface $tagParser, + private readonly TagParserInterface $tagParser, ) {} public function tryParse(string $description): ?DescriptionInterface diff --git a/libs/phpdoc/src/Parser/DocBlockAnalyzer.php b/libs/phpdoc/src/Parser/DocBlockAnalyzer.php index ec2d595..5ef36e0 100644 --- a/libs/phpdoc/src/Parser/DocBlockAnalyzer.php +++ b/libs/phpdoc/src/Parser/DocBlockAnalyzer.php @@ -14,10 +14,10 @@ * A line opening with "@" starts a new tag; any line before the first tag * belongs to the description, and any non-tag line after a tag continues it. */ -final readonly class DocBlockAnalyzer +final class DocBlockAnalyzer { public function __construct( - private SplitterInterface $splitter, + private readonly SplitterInterface $splitter, ) {} public function analyze(string $docblock): RawDocBlock diff --git a/libs/phpdoc/src/Parser/Grammar/Context.php b/libs/phpdoc/src/Parser/Grammar/Context.php index 4d145fc..a355292 100644 --- a/libs/phpdoc/src/Parser/Grammar/Context.php +++ b/libs/phpdoc/src/Parser/Grammar/Context.php @@ -37,7 +37,7 @@ public function capture(string $alias, mixed $value): void */ public function mark(): array { - return [$this->cursor->position, \count($this->captures)]; + return [$this->cursor->getPosition(), \count($this->captures)]; } /** @@ -50,7 +50,7 @@ public function rollback(array $snapshot): void { [$position, $length] = $snapshot; - $this->cursor->position = $position; + $this->cursor->moveTo($position); if (\count($this->captures) > $length) { $this->captures = \array_slice($this->captures, 0, $length); diff --git a/libs/phpdoc/src/Parser/Grammar/Cursor.php b/libs/phpdoc/src/Parser/Grammar/Cursor.php index acd49b1..269c329 100644 --- a/libs/phpdoc/src/Parser/Grammar/Cursor.php +++ b/libs/phpdoc/src/Parser/Grammar/Cursor.php @@ -8,15 +8,15 @@ * A reading position over a tag suffix that grammar rules consume from. * * The reading methods cover the shapes a rule usually needs (a word, an - * identifier, a literal, a run of characters), so a rule rarely has to touch - * {@see $position} by hand that is reserved for rewinding a speculative match. + * identifier, a literal, a run of characters), so a rule rarely has to call + * {@see moveTo()} by hand that is reserved for rewinding a speculative match. */ final class Cursor { /** * The whitespace bytes that separate words. */ - private const string CHARS_WHITESPACE = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"; + private const CHARS_WHITESPACE = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"; /** * @var int<0, max> @@ -24,8 +24,8 @@ final class Cursor private readonly int $length; /** - * The furthest position ever reached, reported by {@see $furthestOffset} - * when a match fails. + * The furthest position ever reached, reported by + * {@see getFurthestOffset()} when a match fails. * * @var int<0, max> */ @@ -34,21 +34,12 @@ final class Cursor /** * The current position within the buffer. * - * Reading advances it; assigning rewinds it to roll a speculative match - * back. Whatever it is set to, the furthest position reached is remembered - * for failure reporting. + * Reading advances it, {@see moveTo()} rewinds it to roll a speculative + * match back. * * @var int<0, max> */ - public int $position = 0 { - set(int $position) { - $this->position = $position; - - if ($position > $this->furthest) { - $this->furthest = $position; - } - } - } + private int $current = 0; /** * List of PHP identifier chars as string @@ -71,26 +62,54 @@ public function __construct( self::$phpIdentifierChars ??= self::createIdentifierChars(); } + /** + * The current position within the buffer. + * + * @return int<0, max> + */ + public function getPosition(): int + { + return $this->current; + } + + /** + * Rewinds the cursor to roll a speculative match back. Whatever it is set + * to, the furthest position reached is remembered for failure reporting. + * + * @param int<0, max> $position + */ + public function moveTo(int $position): void + { + $this->current = $position; + + if ($position > $this->furthest) { + $this->furthest = $position; + } + } + /** * The current byte offset within the source. * - * @var int<0, max> + * @return int<0, max> */ - public int $offset { - get => $this->base + $this->position; + public function getOffset(): int + { + return $this->base + $this->current; } /** * The byte offset to report when a match fails. * - * @var int<0, max> + * @return int<0, max> */ - public int $furthestOffset { - get => $this->base + $this->furthest; + public function getFurthestOffset(): int + { + return $this->base + $this->furthest; } - public bool $isEof { - get => $this->position >= $this->length; + public function isEof(): bool + { + return $this->current >= $this->length; } /** @@ -98,7 +117,7 @@ public function __construct( */ public function peek(int $length = 1): string { - return \substr($this->buffer, $this->position, \max(0, $length)); + return \substr($this->buffer, $this->current, \max(0, $length)); } /** @@ -106,8 +125,9 @@ public function peek(int $length = 1): string */ public function read(int $length): string { - $value = \substr($this->buffer, $this->position, \max(0, $length)); - $this->position += \strlen($value); + $value = \substr($this->buffer, $this->current, \max(0, $length)); + + $this->moveTo($this->current + \strlen($value)); return $value; } @@ -118,7 +138,7 @@ public function read(int $length): string */ public function readWhile(string $characters): string { - return $this->read(\strspn($this->buffer, $characters, $this->position)); + return $this->read(\strspn($this->buffer, $characters, $this->current)); } /** @@ -128,7 +148,7 @@ public function readWhile(string $characters): string */ public function readUntil(string $characters): string { - return $this->read(\strcspn($this->buffer, $characters, $this->position)); + return $this->read(\strcspn($this->buffer, $characters, $this->current)); } /** @@ -170,13 +190,13 @@ public function readLiteral(string $literal): bool return true; } - if ($this->position + $length > $this->length - || \substr_compare($this->buffer, $literal, $this->position, $length) !== 0 + if ($this->current + $length > $this->length + || \substr_compare($this->buffer, $literal, $this->current, $length) !== 0 ) { return false; } - $this->position += $length; + $this->moveTo($this->current + $length); return true; } @@ -186,7 +206,7 @@ public function readLiteral(string $literal): bool */ public function skipWhitespace(): void { - $this->position += \strspn($this->buffer, self::CHARS_WHITESPACE, $this->position); + $this->moveTo($this->current + \strspn($this->buffer, self::CHARS_WHITESPACE, $this->current)); } /** @@ -194,8 +214,9 @@ public function skipWhitespace(): void */ public function readRemainder(): string { - $rest = \substr($this->buffer, $this->position); - $this->position = $this->length; + $rest = \substr($this->buffer, $this->current); + + $this->moveTo($this->length); return $rest; } diff --git a/libs/phpdoc/src/Parser/Grammar/Grammar.php b/libs/phpdoc/src/Parser/Grammar/Grammar.php index a2337c8..61b8650 100644 --- a/libs/phpdoc/src/Parser/Grammar/Grammar.php +++ b/libs/phpdoc/src/Parser/Grammar/Grammar.php @@ -42,7 +42,9 @@ final class Grammar implements \Countable, \IteratorAggregate */ public function __construct(iterable $rules = []) { - $this->combinators = \iterator_to_array($rules); + $this->combinators = \is_array($rules) + ? $rules + : \iterator_to_array($rules); } /** diff --git a/libs/phpdoc/src/Parser/Grammar/Rule/AlternationRule.php b/libs/phpdoc/src/Parser/Grammar/Rule/AlternationRule.php index ebee098..0c12358 100644 --- a/libs/phpdoc/src/Parser/Grammar/Rule/AlternationRule.php +++ b/libs/phpdoc/src/Parser/Grammar/Rule/AlternationRule.php @@ -10,12 +10,12 @@ /** * Matches the first of the given alternatives that applies. */ -final readonly class AlternationRule implements ProductionInterface +final class AlternationRule implements ProductionInterface { /** * @var non-empty-list */ - private array $rules; + private readonly array $rules; public function __construct(RuleInterface $rule, RuleInterface ...$other) { diff --git a/libs/phpdoc/src/Parser/Grammar/Rule/LiteralRule.php b/libs/phpdoc/src/Parser/Grammar/Rule/LiteralRule.php index d9fa6dd..ddfdabc 100644 --- a/libs/phpdoc/src/Parser/Grammar/Rule/LiteralRule.php +++ b/libs/phpdoc/src/Parser/Grammar/Rule/LiteralRule.php @@ -10,17 +10,17 @@ /** * Matches an exact literal at the current position. */ -final readonly class LiteralRule implements TerminalInterface +final class LiteralRule implements TerminalInterface { public function __construct( /** * @var non-empty-string */ - private string $value, + private readonly string $value, /** * @var non-empty-string|null */ - public ?string $alias = null, + public readonly ?string $alias = null, ) {} public function match(Context $context): void diff --git a/libs/phpdoc/src/Parser/Grammar/Rule/MatchRule.php b/libs/phpdoc/src/Parser/Grammar/Rule/MatchRule.php index 8b1f837..6a07a88 100644 --- a/libs/phpdoc/src/Parser/Grammar/Rule/MatchRule.php +++ b/libs/phpdoc/src/Parser/Grammar/Rule/MatchRule.php @@ -15,7 +15,7 @@ * new MatchRule('URI'); // matches but captures nothing * ``` */ -final readonly class MatchRule implements TerminalInterface +final class MatchRule implements TerminalInterface { public function __construct( /** @@ -23,15 +23,15 @@ public function __construct( * * @var non-empty-string */ - private string $rule, + private readonly string $rule, /** * @var non-empty-string|null */ - public ?string $alias = null, + public readonly ?string $alias = null, /** * @var non-empty-string|null */ - public ?string $renderAs = null, + public readonly ?string $renderAs = null, ) {} public function match(Context $context): void diff --git a/libs/phpdoc/src/Parser/Grammar/Rule/OptionalityRule.php b/libs/phpdoc/src/Parser/Grammar/Rule/OptionalityRule.php index 82f87ac..15a5350 100644 --- a/libs/phpdoc/src/Parser/Grammar/Rule/OptionalityRule.php +++ b/libs/phpdoc/src/Parser/Grammar/Rule/OptionalityRule.php @@ -10,10 +10,10 @@ /** * Matches an inner rule when it applies, and is skipped when it does not. */ -final readonly class OptionalityRule implements ProductionInterface +final class OptionalityRule implements ProductionInterface { public function __construct( - private RuleInterface $rule, + private readonly RuleInterface $rule, ) {} public function match(Context $context): void diff --git a/libs/phpdoc/src/Parser/Grammar/Rule/RepetitionRule.php b/libs/phpdoc/src/Parser/Grammar/Rule/RepetitionRule.php index 876514d..f671c8d 100644 --- a/libs/phpdoc/src/Parser/Grammar/Rule/RepetitionRule.php +++ b/libs/phpdoc/src/Parser/Grammar/Rule/RepetitionRule.php @@ -10,16 +10,16 @@ /** * Matches an inner rule as many times as it applies. */ -final readonly class RepetitionRule implements ProductionInterface +final class RepetitionRule implements ProductionInterface { public function __construct( - private RuleInterface $rule, + private readonly RuleInterface $rule, /** * Least number of repetitions required to match. * * @var int<0, max> */ - private int $min = 0, + private readonly int $min = 0, ) {} public function match(Context $context): void @@ -43,7 +43,7 @@ public function match(Context $context): void ++$count; - if ($context->cursor->position === $snapshot[0]) { + if ($context->cursor->getPosition() === $snapshot[0]) { break; } } diff --git a/libs/phpdoc/src/Parser/Grammar/Rule/SequencingRule.php b/libs/phpdoc/src/Parser/Grammar/Rule/SequencingRule.php index 9240ea3..1b29b52 100644 --- a/libs/phpdoc/src/Parser/Grammar/Rule/SequencingRule.php +++ b/libs/phpdoc/src/Parser/Grammar/Rule/SequencingRule.php @@ -10,12 +10,12 @@ /** * Matches an ordered sequence of rules, one after another. */ -final readonly class SequencingRule implements ProductionInterface +final class SequencingRule implements ProductionInterface { /** * @var non-empty-list */ - private array $rules; + private readonly array $rules; public function __construct(RuleInterface $rule, RuleInterface ...$other) { diff --git a/libs/phpdoc/src/Parser/Grammar/Rule/TerminalInterface.php b/libs/phpdoc/src/Parser/Grammar/Rule/TerminalInterface.php index 604c924..89a3b47 100644 --- a/libs/phpdoc/src/Parser/Grammar/Rule/TerminalInterface.php +++ b/libs/phpdoc/src/Parser/Grammar/Rule/TerminalInterface.php @@ -6,16 +6,7 @@ /** * Interface denoting a leaf (that is a terminal) rule. + * + * @property-read ?non-empty-string $alias The name the matched value is captured under, if any. */ -interface TerminalInterface extends RuleInterface -{ - /** - * Capture name under which the presence of the rule is recorded, - * or {@see null} to match without capturing. - * - * @var non-empty-string|null - */ - public ?string $alias { - get; - } -} +interface TerminalInterface extends RuleInterface {} diff --git a/libs/phpdoc/src/Parser/RawDocBlock.php b/libs/phpdoc/src/Parser/RawDocBlock.php index 737c390..e92ec29 100644 --- a/libs/phpdoc/src/Parser/RawDocBlock.php +++ b/libs/phpdoc/src/Parser/RawDocBlock.php @@ -6,13 +6,13 @@ use TypeLang\PhpDoc\Parser\Splitter\Segment; -final readonly class RawDocBlock +final class RawDocBlock { public function __construct( - public ?Segment $description, + public readonly ?Segment $description, /** * @var list */ - public array $tags, + public readonly array $tags, ) {} } diff --git a/libs/phpdoc/src/Parser/Splitter/StringSplitter.php b/libs/phpdoc/src/Parser/Splitter/StringSplitter.php index 77cb194..c986561 100644 --- a/libs/phpdoc/src/Parser/Splitter/StringSplitter.php +++ b/libs/phpdoc/src/Parser/Splitter/StringSplitter.php @@ -4,39 +4,39 @@ namespace TypeLang\PhpDoc\Parser\Splitter; -final readonly class StringSplitter implements SplitterInterface +final class StringSplitter implements SplitterInterface { /** * @var non-empty-string */ - private const string SEQUENCE_OPENING = '/*'; + private const SEQUENCE_OPENING = '/*'; /** * @var non-empty-string */ - private const string SEQUENCE_OPENING_DOCBLOCK = '/**'; + private const SEQUENCE_OPENING_DOCBLOCK = '/**'; /** * @var non-empty-string */ - private const string SEQUENCE_CLOSING = '*/'; + private const SEQUENCE_CLOSING = '*/'; /** * @var non-empty-string */ - private const string SEQUENCE_BODY = '*'; + private const SEQUENCE_BODY = '*'; /** * @var non-empty-string */ - private const string LINE_TERMINATORS = "\r\n"; + private const LINE_TERMINATORS = "\r\n"; /** * Characters trimmed from the head of a line before its content begins. * * @var non-empty-string */ - private const string BLANK = " \t\n\r\0\x0B"; + private const BLANK = " \t\n\r\0\x0B"; /** * Trailing whitespace trimmed from a line: {@see BLANK} without the line @@ -44,7 +44,7 @@ * * @var non-empty-string */ - private const string TRAILING_WHITESPACE = " \t\0\x0B"; + private const TRAILING_WHITESPACE = " \t\0\x0B"; /** * @return list diff --git a/libs/phpdoc/src/Parser/Tag/StringTagParser.php b/libs/phpdoc/src/Parser/Tag/StringTagParser.php index 6da4f19..f65491b 100644 --- a/libs/phpdoc/src/Parser/Tag/StringTagParser.php +++ b/libs/phpdoc/src/Parser/Tag/StringTagParser.php @@ -28,22 +28,22 @@ * A definition that does not start with an "@", or whose "@" is not followed * by a name, is an {@see InvalidTag}. */ -final readonly class StringTagParser implements TagParserInterface +final class StringTagParser implements TagParserInterface { /** * The ASCII characters allowed inside a tag name. * * @var non-empty-string */ - private const string ASCII_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyz' + private const ASCII_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyz' . 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' . '0123456789' . '_-\\:'; - private string $nameTerminators; + private readonly string $nameTerminators; public function __construct( - private TagFactoryInterface $tagFactory, + private readonly TagFactoryInterface $tagFactory, ) { $this->nameTerminators = self::createTerminatorMask(); } diff --git a/libs/phpdoc/src/Parser/TagFactory.php b/libs/phpdoc/src/Parser/TagFactory.php index bc4a45d..f256bca 100644 --- a/libs/phpdoc/src/Parser/TagFactory.php +++ b/libs/phpdoc/src/Parser/TagFactory.php @@ -28,15 +28,15 @@ * * @phpstan-import-type CombinatorType from CombinatorInterface */ -final readonly class TagFactory implements TagFactoryInterface +final class TagFactory implements TagFactoryInterface { - private TagSpecificationParser $parser; + private readonly TagSpecificationParser $parser; /** * @param iterable $combinators */ public function __construct( - private TagRegistryInterface $registry, + private readonly TagRegistryInterface $registry, iterable $combinators = [], ) { $this->parser = new TagSpecificationParser($combinators); diff --git a/libs/phpdoc/src/Parser/TagRegistry.php b/libs/phpdoc/src/Parser/TagRegistry.php index 3251f4e..2b932a5 100644 --- a/libs/phpdoc/src/Parser/TagRegistry.php +++ b/libs/phpdoc/src/Parser/TagRegistry.php @@ -26,14 +26,14 @@ * * @template-implements \IteratorAggregate */ -final readonly class TagRegistry implements TagRegistryInterface, \IteratorAggregate +final class TagRegistry implements TagRegistryInterface, \IteratorAggregate { public function __construct( /** * @var array */ - private array $definitions, - private TagDefinitionInterface $genericTagDefinition, + private readonly array $definitions, + private readonly TagDefinitionInterface $genericTagDefinition, ) {} public function has(string $name): bool diff --git a/libs/phpdoc/src/Parser/TagSpecificationParser.php b/libs/phpdoc/src/Parser/TagSpecificationParser.php index 9a80f76..41a2e55 100644 --- a/libs/phpdoc/src/Parser/TagSpecificationParser.php +++ b/libs/phpdoc/src/Parser/TagSpecificationParser.php @@ -18,9 +18,9 @@ /** * @phpstan-import-type CombinatorType from CombinatorInterface */ -final readonly class TagSpecificationParser +final class TagSpecificationParser { - private Grammar $grammar; + private readonly Grammar $grammar; /** * @param iterable $combinators @@ -49,7 +49,7 @@ public function parse(TagDefinitionInterface $definition, string $name, string $ tag: $name, grammar: (string) $rule, source: $suffix, - offset: $cursor->furthestOffset, + offset: $cursor->getFurthestOffset(), ); } diff --git a/libs/phpdoc/src/Platform/PhanPlatform.php b/libs/phpdoc/src/Platform/PhanPlatform.php index fba14bb..0e668d3 100644 --- a/libs/phpdoc/src/Platform/PhanPlatform.php +++ b/libs/phpdoc/src/Platform/PhanPlatform.php @@ -50,10 +50,11 @@ final class PhanPlatform extends Platform { /** - * @var iterable + * @return iterable */ - public iterable $tags { - get => [ + public function getTags(): iterable + { + return [ 'phan-assert' => new AssertTagDefinition(), 'phan-assert-if-true' => new AssertIfTrueTagDefinition(), 'phan-assert-if-false' => new AssertIfFalseTagDefinition(), @@ -78,10 +79,11 @@ final class PhanPlatform extends Platform } /** - * @var iterable + * @return iterable */ - public iterable $aliases { - get => [ + public function getAliases(): iterable + { + return [ 'phan-abstract' => AbstractTagDefinition::NAME, 'phanclosurescope' => PhanClosureScopeTagDefinition::NAME, 'phan-extends' => ExtendsTagDefinition::NAME, diff --git a/libs/phpdoc/src/Platform/PhpCodeSnifferPlatform.php b/libs/phpdoc/src/Platform/PhpCodeSnifferPlatform.php index 8a7837c..70ee6b7 100644 --- a/libs/phpdoc/src/Platform/PhpCodeSnifferPlatform.php +++ b/libs/phpdoc/src/Platform/PhpCodeSnifferPlatform.php @@ -19,10 +19,11 @@ final class PhpCodeSnifferPlatform extends Platform { /** - * @var iterable + * @return iterable */ - public iterable $tags { - get => [ + public function getTags(): iterable + { + return [ CodingStandardsIgnoreStartTagDefinition::NAME => new CodingStandardsIgnoreStartTagDefinition(), CodingStandardsIgnoreEndTagDefinition::NAME => new CodingStandardsIgnoreEndTagDefinition(), CodingStandardsIgnoreLineTagDefinition::NAME => new CodingStandardsIgnoreLineTagDefinition(), diff --git a/libs/phpdoc/src/Platform/PhpDocumentorPlatform.php b/libs/phpdoc/src/Platform/PhpDocumentorPlatform.php index 17d7704..8cc57b5 100644 --- a/libs/phpdoc/src/Platform/PhpDocumentorPlatform.php +++ b/libs/phpdoc/src/Platform/PhpDocumentorPlatform.php @@ -43,10 +43,11 @@ final class PhpDocumentorPlatform extends Platform { /** - * @var iterable + * @return iterable */ - public iterable $tags { - get => [ + public function getTags(): iterable + { + return [ AccessTagDefinition::NAME => new AccessTagDefinition(), AuthorTagDefinition::NAME => new AuthorTagDefinition(), CategoryTagDefinition::NAME => new CategoryTagDefinition(), @@ -72,10 +73,11 @@ final class PhpDocumentorPlatform extends Platform } /** - * @var iterable + * @return iterable */ - public iterable $combinators { - get => [ + public function getCombinators(): iterable + { + return [ VisibilityCombinator::NAME => new VisibilityCombinator(), AuthorNameCombinator::NAME => new AuthorNameCombinator(), EmailCombinator::NAME => new EmailCombinator(), diff --git a/libs/phpdoc/src/Platform/PhpStanPlatform.php b/libs/phpdoc/src/Platform/PhpStanPlatform.php index 875fd6b..aa65f78 100644 --- a/libs/phpdoc/src/Platform/PhpStanPlatform.php +++ b/libs/phpdoc/src/Platform/PhpStanPlatform.php @@ -53,10 +53,11 @@ final class PhpStanPlatform extends Platform { /** - * @var iterable + * @return iterable */ - public iterable $tags { - get => [ + public function getTags(): iterable + { + return [ 'phpstan-allow-private-mutation' => new AllowPrivateMutationTagDefinition(), 'phpstan-assert' => new AssertTagDefinition(), 'phpstan-assert-if-true' => new AssertIfTrueTagDefinition(), @@ -76,10 +77,11 @@ final class PhpStanPlatform extends Platform } /** - * @var iterable + * @return iterable */ - public iterable $aliases { - get => [ + public function getAliases(): iterable + { + return [ 'phpstan-extends' => ExtendsTagDefinition::NAME, 'phpstan-immutable' => ImmutableTagDefinition::NAME, 'phpstan-implements' => ImplementsTagDefinition::NAME, diff --git a/libs/phpdoc/src/Platform/PhpStormPlatform.php b/libs/phpdoc/src/Platform/PhpStormPlatform.php index 7a7b967..20fe377 100644 --- a/libs/phpdoc/src/Platform/PhpStormPlatform.php +++ b/libs/phpdoc/src/Platform/PhpStormPlatform.php @@ -17,10 +17,11 @@ final class PhpStormPlatform extends Platform { /** - * @var iterable + * @return iterable */ - public iterable $tags { - get => [ + public function getTags(): iterable + { + return [ ExpectedExceptionTagDefinition::NAME => new ExpectedExceptionTagDefinition(), FormatterOffTagDefinition::NAME => new FormatterOffTagDefinition(), FormatterOnTagDefinition::NAME => new FormatterOnTagDefinition(), diff --git a/libs/phpdoc/src/Platform/Platform.php b/libs/phpdoc/src/Platform/Platform.php index e80820c..29decc2 100644 --- a/libs/phpdoc/src/Platform/Platform.php +++ b/libs/phpdoc/src/Platform/Platform.php @@ -13,23 +13,26 @@ abstract class Platform implements PlatformInterface { /** - * @var iterable + * @return iterable */ - public iterable $tags { - get => []; + public function getTags(): iterable + { + return []; } /** - * @var iterable + * @return iterable */ - public iterable $aliases { - get => []; + public function getAliases(): iterable + { + return []; } /** - * @var iterable + * @return iterable */ - public iterable $combinators { - get => []; + public function getCombinators(): iterable + { + return []; } } diff --git a/libs/phpdoc/src/Platform/PlatformInterface.php b/libs/phpdoc/src/Platform/PlatformInterface.php index ce30d7a..f00be3f 100644 --- a/libs/phpdoc/src/Platform/PlatformInterface.php +++ b/libs/phpdoc/src/Platform/PlatformInterface.php @@ -21,27 +21,21 @@ interface PlatformInterface /** * Tag definitions keyed by their canonical (lower-case) name. * - * @var iterable + * @return iterable */ - public iterable $tags { - get; - } + public function getTags(): iterable; /** * Alias-to-canonical name pairs, both lower-case. * - * @var iterable + * @return iterable */ - public iterable $aliases { - get; - } + public function getAliases(): iterable; /** * Grammar combinators keyed by their name. * - * @var iterable + * @return iterable */ - public iterable $combinators { - get; - } + public function getCombinators(): iterable; } diff --git a/libs/phpdoc/src/Platform/PsalmPlatform.php b/libs/phpdoc/src/Platform/PsalmPlatform.php index 82cd169..4383532 100644 --- a/libs/phpdoc/src/Platform/PsalmPlatform.php +++ b/libs/phpdoc/src/Platform/PsalmPlatform.php @@ -80,10 +80,11 @@ final class PsalmPlatform extends Platform { /** - * @var iterable + * @return iterable */ - public iterable $tags { - get => [ + public function getTags(): iterable + { + return [ 'psalm-allow-private-mutation' => new AllowPrivateMutationTagDefinition(), 'psalm-assert' => new AssertTagDefinition(), 'psalm-assert-if-true' => new AssertIfTrueTagDefinition(), @@ -126,10 +127,11 @@ final class PsalmPlatform extends Platform } /** - * @var iterable + * @return iterable */ - public iterable $aliases { - get => [ + public function getAliases(): iterable + { + return [ 'psalm-api' => ApiTagDefinition::NAME, 'psalm-extends' => ExtendsTagDefinition::NAME, 'psalm-immutable' => ImmutableTagDefinition::NAME, @@ -158,10 +160,11 @@ final class PsalmPlatform extends Platform } /** - * @var iterable + * @return iterable */ - public iterable $combinators { - get => [ + public function getCombinators(): iterable + { + return [ FlowTypeCombinator::NAME => new FlowTypeCombinator(), ]; } diff --git a/libs/phpdoc/src/Platform/StandardPlatform.php b/libs/phpdoc/src/Platform/StandardPlatform.php index 8aadd2b..af729ce 100644 --- a/libs/phpdoc/src/Platform/StandardPlatform.php +++ b/libs/phpdoc/src/Platform/StandardPlatform.php @@ -6,6 +6,7 @@ use TypeLang\Parser\TypeParser; use TypeLang\Parser\TypeParserInterface; +use TypeLang\PhpDoc\DocBlock\Combinator\AssertSubjectCombinator; use TypeLang\PhpDoc\DocBlock\Combinator\CallableTypeCombinator; use TypeLang\PhpDoc\DocBlock\Combinator\IntegerCombinator; use TypeLang\PhpDoc\DocBlock\Combinator\IssueNameCombinator; @@ -69,10 +70,11 @@ final class StandardPlatform implements PlatformInterface { /** - * @var iterable + * @return iterable */ - public iterable $tags { - get => [ + public function getTags(): iterable + { + return [ LinkTagDefinition::NAME => new LinkTagDefinition(), SeeTagDefinition::NAME => new SeeTagDefinition(), ReturnTagDefinition::NAME => new ReturnTagDefinition(), @@ -115,10 +117,11 @@ final class StandardPlatform implements PlatformInterface } /** - * @var iterable + * @return iterable */ - public iterable $aliases { - get => [ + public function getAliases(): iterable + { + return [ 'inherits' => ExtendsTagDefinition::NAME, 'template-extends' => ExtendsTagDefinition::NAME, 'template-implements' => ImplementsTagDefinition::NAME, @@ -131,16 +134,18 @@ final class StandardPlatform implements PlatformInterface } /** - * @var iterable + * @return iterable */ - public iterable $combinators { - get => [ + public function getCombinators(): iterable + { + return [ UriCombinator::NAME => new UriCombinator(), UrlCombinator::NAME => new UrlCombinator(), ReferenceCombinator::NAME => new ReferenceCombinator(), TypeCombinator::NAME => new TypeCombinator($this->typeParser), CallableTypeCombinator::NAME => new CallableTypeCombinator($this->typeParser), VariableCombinator::NAME => new VariableCombinator(), + AssertSubjectCombinator::NAME => new AssertSubjectCombinator(), IntegerCombinator::NAME => new IntegerCombinator(), IssueNameCombinator::NAME => new IssueNameCombinator(), VersionCombinator::NAME => new VersionCombinator(), diff --git a/libs/phpdoc/tests/Bench/DocBlockParserBench.php b/libs/phpdoc/tests/Bench/DocBlockParserBench.php deleted file mode 100644 index da74224..0000000 --- a/libs/phpdoc/tests/Bench/DocBlockParserBench.php +++ /dev/null @@ -1,28 +0,0 @@ - | ] [] - * ``` - * - * @link https://www.ietf.org/rfc/rfc2396.txt RFC2396 - * @return iterable }> - * }> - */ - DOC; - - abstract public function benchParseDocBlock(): void; -} diff --git a/libs/phpdoc/tests/Bench/PhpDocumentorParserBench.php b/libs/phpdoc/tests/Bench/PhpDocumentorParserBench.php deleted file mode 100644 index b5210b3..0000000 --- a/libs/phpdoc/tests/Bench/PhpDocumentorParserBench.php +++ /dev/null @@ -1,29 +0,0 @@ -parser = DocBlockFactory::createInstance(); - } - - public function benchParseDocBlock(): void - { - $this->parser->create(self::DOC_BLOCK_SAMPLE); - } -} diff --git a/libs/phpdoc/tests/Bench/TypeLangParserBench.php b/libs/phpdoc/tests/Bench/TypeLangParserBench.php deleted file mode 100644 index e2628e5..0000000 --- a/libs/phpdoc/tests/Bench/TypeLangParserBench.php +++ /dev/null @@ -1,29 +0,0 @@ -parser = new DocBlockParser(); - } - - public function benchParseDocBlock(): void - { - $this->parser->parse(self::DOC_BLOCK_SAMPLE); - } -} diff --git a/libs/phpdoc/tests/DocBlock/Description/DescriptionTest.php b/libs/phpdoc/tests/DocBlock/Description/DescriptionTest.php index a9d9d8c..8a9bc14 100644 --- a/libs/phpdoc/tests/DocBlock/Description/DescriptionTest.php +++ b/libs/phpdoc/tests/DocBlock/Description/DescriptionTest.php @@ -22,7 +22,7 @@ public function constructorStoresValue(): void #[Test] public function valueDefaultsToEmptyString(): void { - self::assertSame('', new Description()->value); + self::assertSame('', (new Description())->value); } #[Test] diff --git a/libs/phpdoc/tests/DocBlock/Description/TaggedDescriptionTest.php b/libs/phpdoc/tests/DocBlock/Description/TaggedDescriptionTest.php index f94f955..76ea5c2 100644 --- a/libs/phpdoc/tests/DocBlock/Description/TaggedDescriptionTest.php +++ b/libs/phpdoc/tests/DocBlock/Description/TaggedDescriptionTest.php @@ -27,7 +27,7 @@ public function constructorStoresComponentsAsList(): void #[Test] public function componentsDefaultToEmptyList(): void { - self::assertSame([], new TaggedDescription()->components); + self::assertSame([], (new TaggedDescription())->components); } #[Test] @@ -95,7 +95,7 @@ public function offsetGetReturnsComponent(): void #[Test] public function offsetGetReturnsNullForMissingOffset(): void { - self::assertNull(new TaggedDescription()[42]); + self::assertNull((new TaggedDescription())[42]); } #[Test] @@ -124,7 +124,7 @@ public function iteratorYieldsAllComponentsInOrder(): void $components = [new Description('a'), new Tag('see'), new Description('b')]; $description = new TaggedDescription($components); - self::assertSame($components, \iterator_to_array($description, false)); + self::assertSame($components, \iterator_to_array($description->getIterator(), false)); } #[Test] diff --git a/libs/phpdoc/tests/DocBlock/DocBlockTest.php b/libs/phpdoc/tests/DocBlock/DocBlockTest.php index 129c2dc..3d1d52b 100644 --- a/libs/phpdoc/tests/DocBlock/DocBlockTest.php +++ b/libs/phpdoc/tests/DocBlock/DocBlockTest.php @@ -15,7 +15,7 @@ final class DocBlockTest extends TestCase #[Test] public function descriptionDefaultsToNull(): void { - self::assertNull(new DocBlock()->description); + self::assertNull((new DocBlock())->description); } #[Test] @@ -23,13 +23,13 @@ public function constructorKeepsExistingDescriptionInstance(): void { $description = new Description('Summary'); - self::assertSame($description, new DocBlock($description)->description); + self::assertSame($description, (new DocBlock($description))->description); } #[Test] public function tagsDefaultToEmptyList(): void { - self::assertSame([], new DocBlock()->tags); + self::assertSame([], (new DocBlock())->tags); } #[Test] @@ -82,7 +82,7 @@ public function offsetGetReturnsTag(): void #[Test] public function offsetGetReturnsNullForMissingOffset(): void { - self::assertNull(new DocBlock()[42]); + self::assertNull((new DocBlock())[42]); } #[Test] @@ -111,6 +111,6 @@ public function iteratorYieldsAllTagsInOrder(): void $tags = [new Tag('param'), new Tag('return')]; $docblock = new DocBlock(null, $tags); - self::assertSame($tags, \iterator_to_array($docblock, false)); + self::assertSame($tags, \iterator_to_array($docblock->getIterator(), false)); } } diff --git a/libs/phpdoc/tests/DocBlock/Grammar/DescriptionGrammarRuleTest.php b/libs/phpdoc/tests/DocBlock/Grammar/DescriptionGrammarRuleTest.php index befd0e7..dfb0942 100644 --- a/libs/phpdoc/tests/DocBlock/Grammar/DescriptionGrammarRuleTest.php +++ b/libs/phpdoc/tests/DocBlock/Grammar/DescriptionGrammarRuleTest.php @@ -21,23 +21,28 @@ final class DescriptionGrammarRuleTest extends GrammarRuleTestCase { protected function rule(): DescriptionCombinator { - return new \ReflectionClass(DescriptionCombinator::class) - ->newLazyProxy(function (DescriptionCombinator $proxy) { - $registry = new TagRegistry( - definitions: [], - genericTagDefinition: new GenericTagDefinition(), - ); - - return new DescriptionCombinator( - descriptionParser: new BalancedBraceAwareParser( - tagParser: new StringTagParser( - tagFactory: new TagFactory($registry, [ - DescriptionCombinator::NAME => $proxy, - ]), - ), - ), - ); - }); + $registry = new TagRegistry( + definitions: [], + genericTagDefinition: new GenericTagDefinition(), + ); + + $rule = null; + + $nested = static function (Cursor $cursor) use (&$rule): mixed { + \assert($rule instanceof DescriptionCombinator); + + return $rule($cursor); + }; + + return $rule = new DescriptionCombinator( + descriptionParser: new BalancedBraceAwareParser( + tagParser: new StringTagParser( + tagFactory: new TagFactory($registry, [ + DescriptionCombinator::NAME => $nested, + ]), + ), + ), + ); } #[Test] @@ -66,7 +71,7 @@ public function consumesTheEntireRemainder(): void self::assertInstanceOf(Description::class, $description); self::assertSame('a b c', (string) $description); - self::assertTrue($cursor->isEof); + self::assertTrue($cursor->isEof()); } /** diff --git a/libs/phpdoc/tests/DocBlock/Grammar/ReferenceGrammarRuleTest.php b/libs/phpdoc/tests/DocBlock/Grammar/ReferenceGrammarRuleTest.php index af98152..e350f7d 100644 --- a/libs/phpdoc/tests/DocBlock/Grammar/ReferenceGrammarRuleTest.php +++ b/libs/phpdoc/tests/DocBlock/Grammar/ReferenceGrammarRuleTest.php @@ -69,7 +69,7 @@ public function stopsAtTheFirstWhitespace(): void self::assertInstanceOf(VariableReference::class, $reference); self::assertSame('var', $reference->name); - self::assertSame(4, $cursor->offset); + self::assertSame(4, $cursor->getOffset()); } #[Test] @@ -79,7 +79,7 @@ public function stopsAfterTheReference(): void $reference = $this->matchCursor($cursor); self::assertInstanceOf(ClassMethodReference::class, $reference); - self::assertSame(21, $cursor->offset); + self::assertSame(21, $cursor->getOffset()); } /** diff --git a/libs/phpdoc/tests/DocBlock/Grammar/TypeGrammarRuleTest.php b/libs/phpdoc/tests/DocBlock/Grammar/TypeGrammarRuleTest.php index dfd30a1..5d21172 100644 --- a/libs/phpdoc/tests/DocBlock/Grammar/TypeGrammarRuleTest.php +++ b/libs/phpdoc/tests/DocBlock/Grammar/TypeGrammarRuleTest.php @@ -65,7 +65,7 @@ public function stopsAfterTheType(): void self::assertInstanceOf(UnionTypeNode::class, $statement->type); self::assertSame('int|string', $statement->source); - self::assertSame(11, $cursor->offset); + self::assertSame(11, $cursor->getOffset()); } #[Test] @@ -75,7 +75,7 @@ public function respectsTheCursorBase(): void $statement = $this->matchCursor($cursor); self::assertInstanceOf(TypeReference::class, $statement); - self::assertSame(104, $cursor->offset); + self::assertSame(104, $cursor->getOffset()); } #[Test] diff --git a/libs/phpdoc/tests/DocBlock/Grammar/VariableGrammarRuleTest.php b/libs/phpdoc/tests/DocBlock/Grammar/VariableGrammarRuleTest.php index d3b0a10..dfdc5c6 100644 --- a/libs/phpdoc/tests/DocBlock/Grammar/VariableGrammarRuleTest.php +++ b/libs/phpdoc/tests/DocBlock/Grammar/VariableGrammarRuleTest.php @@ -43,7 +43,7 @@ public function stopsAtTheFirstWhitespace(): void $name = $this->matchCursor($cursor); self::assertSame('var', $name); - self::assertSame(4, $cursor->offset); + self::assertSame(4, $cursor->getOffset()); } /** @@ -56,7 +56,7 @@ public static function invalidDataProvider(): iterable yield 'missing dollar' => ['var']; yield 'dollar only' => ['$']; yield 'illegal character' => ['$foo!bar']; - yield 'namespace separator' => ['$foo\\bar']; + yield 'namespace separator' => ['$foo\$tag->typear']; } #[Test] diff --git a/libs/phpdoc/tests/DocBlock/Tag/AssertionTagTest.php b/libs/phpdoc/tests/DocBlock/Tag/AssertionTagTest.php new file mode 100644 index 0000000..979281f --- /dev/null +++ b/libs/phpdoc/tests/DocBlock/Tag/AssertionTagTest.php @@ -0,0 +1,171 @@ +type); + self::assertSame(AssertOperator::Is, $tag->operator); + self::assertInstanceOf(VariableReference::class, $tag->subject); + self::assertSame('name', $tag->subject->name); + self::assertSame('The name it carries.', (string) $tag->description); + self::assertSame('@phpstan-assert non-empty-string $name The name it carries.', (string) $tag); + } + + /** + * @param non-empty-string $tag + */ + #[Test] + #[DataProvider('operatorProvider')] + public function readsThePrefixOfTheType(string $tag, AssertOperator $expected): void + { + $result = self::parseTag($tag); + + self::assertInstanceOf(AssertionTag::class, $result); + self::assertSame($expected, $result->operator); + self::assertSame($tag, (string) $result); + } + + /** + * @return iterable + */ + public static function operatorProvider(): iterable + { + yield 'of the type' => ['@phpstan-assert int $value', AssertOperator::Is]; + yield 'anything but the type' => ['@phpstan-assert !null $value', AssertOperator::IsNot]; + yield 'the very value' => ['@phpstan-assert =non-empty-string $value', AssertOperator::Equals]; + yield 'anything but the value' => ['@phpstan-assert !=null $value', AssertOperator::NotEquals]; + + // The prefixes are read by every member of the family alike + yield 'when the call returns true' => ['@phpstan-assert-if-true !false $value', AssertOperator::IsNot]; + yield 'when the call returns false' => ['@psalm-assert-if-false =int $value', AssertOperator::Equals]; + } + + /** + * @param non-empty-string $tag + * @param class-string $expected + */ + #[Test] + #[DataProvider('subjectProvider')] + public function readsWhatTheAssertionIsWrittenOf(string $tag, string $expected, string $printed): void + { + $result = self::parseTag($tag); + + self::assertInstanceOf(AssertionTag::class, $result); + self::assertInstanceOf($expected, $result->subject); + self::assertSame($printed, (string) $result->subject); + self::assertSame($tag, (string) $result); + } + + /** + * @return iterable + */ + public static function subjectProvider(): iterable + { + yield 'a variable' => [ + '@phpstan-assert int $value', + VariableReference::class, + '$value', + ]; + yield 'a property of a variable' => [ + '@phpstan-assert-if-true !null $this->comparisonFailure', + VariablePropertyReference::class, + '$this->comparisonFailure', + ]; + yield 'a method of a variable' => [ + '@phpstan-assert Some\Any $this->getItems()', + VariableMethodReference::class, + '$this->getItems()', + ]; + yield 'a static property of a class' => [ + '@psalm-assert !null self::$instances', + ClassPropertyReference::class, + 'self::$instances', + ]; + } + + /** + * @param non-empty-string $name + * @param class-string $expected + */ + #[Test] + #[DataProvider('assertionTagProvider')] + public function assertionTagIsRecognized(string $name, string $expected): void + { + $tag = self::parseTag(\sprintf('@%s int $x', $name)); + + self::assertInstanceOf($expected, $tag); + self::assertInstanceOf(AssertionTag::class, $tag); + self::assertSame($name, $tag->name); + self::assertInstanceOf(NamedTypeNode::class, $tag->type); + } + + /** + * @return iterable}> + */ + public static function assertionTagProvider(): iterable + { + // The assert family is shared across Psalm, PHPStan and Phan, each + // contributing it under its own vendor-prefixed name. + yield '@psalm-assert' => ['psalm-assert', AssertTag::class]; + yield '@phpstan-assert' => ['phpstan-assert', AssertTag::class]; + yield '@phan-assert' => ['phan-assert', AssertTag::class]; + yield '@psalm-assert-if-true' => ['psalm-assert-if-true', AssertIfTrueTag::class]; + yield '@phpstan-assert-if-true' => ['phpstan-assert-if-true', AssertIfTrueTag::class]; + yield '@phan-assert-if-true' => ['phan-assert-if-true', AssertIfTrueTag::class]; + yield '@psalm-assert-if-false' => ['psalm-assert-if-false', AssertIfFalseTag::class]; + yield '@phpstan-assert-if-false' => ['phpstan-assert-if-false', AssertIfFalseTag::class]; + yield '@phan-assert-if-false' => ['phan-assert-if-false', AssertIfFalseTag::class]; + } + + /** + * @param non-empty-string $tag + */ + #[Test] + #[DataProvider('malformedProvider')] + public function rejectsWhatIsNoAssertion(string $tag): void + { + $result = self::parseTag($tag); + + self::assertInstanceOf(InvalidTag::class, $result); + self::assertInstanceOf(MalformedTagException::class, $result->reason); + } + + /** + * @return iterable + */ + public static function malformedProvider(): iterable + { + yield 'a type alone' => ['@phpstan-assert int']; + yield 'a subject alone' => ['@phpstan-assert $value']; + yield 'a prefix alone' => ['@phpstan-assert !']; + yield 'a property of nothing' => ['@phpstan-assert int $this->']; + yield 'a class of no property' => ['@phpstan-assert int self::']; + } +} diff --git a/libs/phpdoc/tests/DocBlock/Tag/FlagTagTest.php b/libs/phpdoc/tests/DocBlock/Tag/FlagTagTest.php index 6fbc7bc..c20e7df 100644 --- a/libs/phpdoc/tests/DocBlock/Tag/FlagTagTest.php +++ b/libs/phpdoc/tests/DocBlock/Tag/FlagTagTest.php @@ -93,8 +93,8 @@ public function parsesTrailingTextAsDescription(): void #[Test] public function placementIsRecognized(): void { - self::assertSame(TagPlacement::Any, new InternalTagDefinition()->placement); - self::assertSame(TagPlacement::Block, new AbstractTagDefinition()->placement); + self::assertSame(TagPlacement::Any, (new InternalTagDefinition())->placement); + self::assertSame(TagPlacement::Block, (new AbstractTagDefinition())->placement); } /** diff --git a/libs/phpdoc/tests/DocBlock/Tag/InvalidTagTest.php b/libs/phpdoc/tests/DocBlock/Tag/InvalidTagTest.php index e1accd7..ca8e993 100644 --- a/libs/phpdoc/tests/DocBlock/Tag/InvalidTagTest.php +++ b/libs/phpdoc/tests/DocBlock/Tag/InvalidTagTest.php @@ -30,7 +30,7 @@ public function constructorStoresReason(): void #[Test] public function descriptionDefaultsToNull(): void { - self::assertNull(new InvalidTag(new \RuntimeException(), 'param')->description); + self::assertNull((new InvalidTag(new \RuntimeException(), 'param'))->description); } #[Test] diff --git a/libs/phpdoc/tests/DocBlock/Tag/TagTest.php b/libs/phpdoc/tests/DocBlock/Tag/TagTest.php index ee07809..b6033ad 100644 --- a/libs/phpdoc/tests/DocBlock/Tag/TagTest.php +++ b/libs/phpdoc/tests/DocBlock/Tag/TagTest.php @@ -14,13 +14,13 @@ final class TagTest extends TagTestCase #[Test] public function constructorStoresName(): void { - self::assertSame('param', new Tag('param')->name); + self::assertSame('param', (new Tag('param'))->name); } #[Test] public function descriptionDefaultsToNull(): void { - self::assertNull(new Tag('param')->description); + self::assertNull((new Tag('param'))->description); } #[Test] @@ -28,7 +28,7 @@ public function constructorKeepsExistingDescriptionInstance(): void { $description = new Description('int $a'); - self::assertSame($description, new Tag('param', $description)->description); + self::assertSame($description, (new Tag('param', $description))->description); } #[Test] diff --git a/libs/phpdoc/tests/DocBlock/Tag/TagTestCase.php b/libs/phpdoc/tests/DocBlock/Tag/TagTestCase.php index 4551802..7733a5b 100644 --- a/libs/phpdoc/tests/DocBlock/Tag/TagTestCase.php +++ b/libs/phpdoc/tests/DocBlock/Tag/TagTestCase.php @@ -82,16 +82,16 @@ protected function createPlatform( array $combinators = [], array $aliases = [], ): PlatformInterface { - return new readonly class ('testing', $tags, $aliases, $combinators) implements PlatformInterface { + return new class ('testing', $tags, $aliases, $combinators) implements PlatformInterface { public function __construct( /** @var non-empty-string */ - public string $name, + public readonly string $name, /** @var iterable */ - public iterable $tags, + public readonly iterable $tags, /** @var iterable */ - public iterable $aliases, + public readonly iterable $aliases, /** @var iterable */ - public iterable $combinators, + public readonly iterable $combinators, ) {} }; } diff --git a/libs/phpdoc/tests/DocBlock/Tag/TypedVariableTagTest.php b/libs/phpdoc/tests/DocBlock/Tag/TypedVariableTagTest.php index ceda3e8..b59ecc3 100644 --- a/libs/phpdoc/tests/DocBlock/Tag/TypedVariableTagTest.php +++ b/libs/phpdoc/tests/DocBlock/Tag/TypedVariableTagTest.php @@ -91,16 +91,7 @@ public static function typedVariableTagProvider(): iterable yield '@property-write' => ['property-write', PropertyWriteTag::class]; yield '@global' => ['global', GlobalTag::class]; - // The assert family is shared across Psalm, PHPStan and Phan, each - // contributing it under its own vendor-prefixed name. - yield '@psalm-assert' => ['psalm-assert', AssertTag::class]; - yield '@phpstan-assert' => ['phpstan-assert', AssertTag::class]; - yield '@phan-assert' => ['phan-assert', AssertTag::class]; - yield '@psalm-assert-if-true' => ['psalm-assert-if-true', AssertIfTrueTag::class]; - yield '@phpstan-assert-if-true' => ['phpstan-assert-if-true', AssertIfTrueTag::class]; - yield '@phan-assert-if-true' => ['phan-assert-if-true', AssertIfTrueTag::class]; - yield '@psalm-assert-if-false' => ['psalm-assert-if-false', AssertIfFalseTag::class]; - yield '@phpstan-assert-if-false' => ['phpstan-assert-if-false', AssertIfFalseTag::class]; - yield '@phan-assert-if-false' => ['phan-assert-if-false', AssertIfFalseTag::class]; + // The assert family carries a subject rather than a variable, so it is + // covered by the {@see AssertionTagTest} of its own. } } diff --git a/libs/phpdoc/tests/Parser/Grammar/CursorTest.php b/libs/phpdoc/tests/Parser/Grammar/CursorTest.php index 813af33..26ddc46 100644 --- a/libs/phpdoc/tests/Parser/Grammar/CursorTest.php +++ b/libs/phpdoc/tests/Parser/Grammar/CursorTest.php @@ -16,9 +16,9 @@ public function startsAtTheBeginning(): void { $cursor = new Cursor('example'); - self::assertSame(0, $cursor->position); - self::assertSame(0, $cursor->offset); - self::assertFalse($cursor->isEof); + self::assertSame(0, $cursor->getPosition()); + self::assertSame(0, $cursor->getOffset()); + self::assertFalse($cursor->isEof()); } #[Test] @@ -27,8 +27,8 @@ public function rebasesTheOffsetOntoTheSource(): void $cursor = new Cursor('example', base: 100); $cursor->read(3); - self::assertSame(3, $cursor->position); - self::assertSame(103, $cursor->offset); + self::assertSame(3, $cursor->getPosition()); + self::assertSame(103, $cursor->getOffset()); } #[Test] @@ -38,7 +38,7 @@ public function peeksWithoutConsuming(): void self::assertSame('e', $cursor->peek()); self::assertSame('exam', $cursor->peek(4)); - self::assertSame(0, $cursor->position); + self::assertSame(0, $cursor->getPosition()); } #[Test] @@ -49,7 +49,7 @@ public function readsAFixedLength(): void self::assertSame('exa', $cursor->read(3)); self::assertSame('mple', $cursor->read(100)); self::assertSame('', $cursor->read(1)); - self::assertTrue($cursor->isEof); + self::assertTrue($cursor->isEof()); } #[Test] @@ -119,7 +119,7 @@ public function consumesAMatchingLiteral(): void $cursor = new Cursor('$name'); self::assertTrue($cursor->readLiteral('$')); - self::assertSame(1, $cursor->position); + self::assertSame(1, $cursor->getPosition()); } #[Test] @@ -128,7 +128,7 @@ public function leavesTheCursorOnAMismatchingLiteral(): void $cursor = new Cursor('name'); self::assertFalse($cursor->readLiteral('$')); - self::assertSame(0, $cursor->position); + self::assertSame(0, $cursor->getPosition()); } #[Test] @@ -137,7 +137,7 @@ public function doesNotMatchALiteralPastTheEnd(): void $cursor = new Cursor('ab'); self::assertFalse($cursor->readLiteral('abc')); - self::assertSame(0, $cursor->position); + self::assertSame(0, $cursor->getPosition()); } #[Test] @@ -145,9 +145,9 @@ public function remembersTheFurthestOffset(): void { $cursor = new Cursor('example', base: 10); $cursor->read(5); - $cursor->position = 2; + $cursor->moveTo(2); - self::assertSame(12, $cursor->offset); - self::assertSame(15, $cursor->furthestOffset); + self::assertSame(12, $cursor->getOffset()); + self::assertSame(15, $cursor->getFurthestOffset()); } } diff --git a/libs/phpdoc/tests/Parser/Grammar/DefinitionTest.php b/libs/phpdoc/tests/Parser/Grammar/DefinitionTest.php index 03a05d6..c8888a3 100644 --- a/libs/phpdoc/tests/Parser/Grammar/DefinitionTest.php +++ b/libs/phpdoc/tests/Parser/Grammar/DefinitionTest.php @@ -27,7 +27,7 @@ final class DefinitionTest extends TestCase #[Test] public function ruleStringifiesToItsGrammar(): void { - self::assertSame(' [ ]', (string) new LinkTagDefinition()->spec); + self::assertSame(' [ ]', (string) (new LinkTagDefinition())->spec); } #[Test] diff --git a/libs/phpdoc/tests/Parser/SplitterTest.php b/libs/phpdoc/tests/Parser/SplitterTest.php index d97b22c..898090c 100644 --- a/libs/phpdoc/tests/Parser/SplitterTest.php +++ b/libs/phpdoc/tests/Parser/SplitterTest.php @@ -345,7 +345,9 @@ private static function comment(string ...$lines): string */ private static function segments(iterable $result): array { - return \iterator_to_array($result, false); + return \is_array($result) + ? \array_values($result) + : \iterator_to_array($result, false); } /** diff --git a/libs/phpdoc/tests/TestCase.php b/libs/phpdoc/tests/TestCase.php index 79c2423..20caf1a 100644 --- a/libs/phpdoc/tests/TestCase.php +++ b/libs/phpdoc/tests/TestCase.php @@ -14,6 +14,7 @@ use TypeLang\PhpDoc\DocBlock\Combinator\VariableCombinator; use TypeLang\PhpDoc\Parser\Description\BalancedBraceAwareParser; use TypeLang\PhpDoc\Parser\Description\DescriptionParserInterface; +use TypeLang\PhpDoc\Parser\Grammar\Cursor; use TypeLang\PhpDoc\Parser\Tag\StringTagParser; use TypeLang\PhpDoc\Parser\TagFactory; use TypeLang\PhpDoc\Parser\TagRegistryBuilder; @@ -42,32 +43,20 @@ private static function buildTagFactory(): TagFactoryInterface ]; $tagFactory = null; + $description = null; - $baseRules[DescriptionCombinator::NAME] = new \ReflectionClass(DescriptionCombinator::class) - ->newLazyProxy(function () use (&$tagFactory): DescriptionCombinator { - if ($tagFactory === null) { - return new DescriptionCombinator(new BalancedBraceAwareParser( - new StringTagParser(new TagFactory( - registry: new TagRegistryBuilder() - ->build(), - combinators: [ - UriCombinator::NAME => new UriCombinator(), - ReferenceCombinator::NAME => new ReferenceCombinator(), - TypeCombinator::NAME => new TypeCombinator(typeParser: new TypeParser()), - VariableCombinator::NAME => new VariableCombinator(), - ], - )), - )); - } - - return new DescriptionCombinator( - new BalancedBraceAwareParser(new StringTagParser($tagFactory)), - ); - }); + $baseRules[DescriptionCombinator::NAME] = static function (Cursor $cursor) use ( + &$tagFactory, + &$description, + ): mixed { + $description ??= new DescriptionCombinator( + new BalancedBraceAwareParser(new StringTagParser($tagFactory)), + ); - $tagFactory = new TagFactory(new TagRegistryBuilder()->build(), $baseRules); + return $description($cursor); + }; - return $tagFactory; + return $tagFactory = new TagFactory((new TagRegistryBuilder())->build(), $baseRules); } protected static function createDescriptionParser(): DescriptionParserInterface diff --git a/libs/printer/.github/workflows/tests.yml b/libs/printer/.github/workflows/tests.yml index a7df15d..3abc5cb 100644 --- a/libs/printer/.github/workflows/tests.yml +++ b/libs/printer/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - php: [ '8.4', '8.5' ] + php: [ '8.1', '8.2', '8.3', '8.4', '8.5' ] os: [ ubuntu-latest, macos-latest, windows-latest ] stability: [ lowest, stable ] steps: diff --git a/libs/printer/README.md b/libs/printer/README.md index 65eac83..4c634d0 100644 --- a/libs/printer/README.md +++ b/libs/printer/README.md @@ -36,7 +36,7 @@ composer require type-lang/printer ``` **Requirements:** -- PHP 8.4+ +- PHP 8.1+ ## Usage diff --git a/libs/printer/composer.json b/libs/printer/composer.json index 75d99c8..2da22f3 100644 --- a/libs/printer/composer.json +++ b/libs/printer/composer.json @@ -9,7 +9,7 @@ "issues": "https://github.com/php-type-language/printer/issues" }, "require": { - "php": "^8.4", + "php": "^8.1", "type-lang/types": "^2.0" }, "autoload": { @@ -19,8 +19,8 @@ }, "require-dev": { "type-lang/parser": "^2.0", - "phpstan/phpstan": "^2.2.5", - "phpunit/phpunit": "^13.2.4" + "phpstan/phpstan": "^2.2", + "phpunit/phpunit": "^10.5|^11.0|^12.0|^13.0" }, "autoload-dev": { "psr-4": { diff --git a/libs/printer/phpstan.neon b/libs/printer/phpstan.neon index 018e46e..f2d4b90 100644 --- a/libs/printer/phpstan.neon +++ b/libs/printer/phpstan.neon @@ -1,5 +1,9 @@ parameters: level: max + phpVersion: + min: 80100 + max: 80599 + treatPhpDocTypesAsCertain: false fileExtensions: - php paths: diff --git a/libs/printer/src/NativeTypePrinter.php b/libs/printer/src/NativeTypePrinter.php index 5196520..72e2534 100644 --- a/libs/printer/src/NativeTypePrinter.php +++ b/libs/printer/src/NativeTypePrinter.php @@ -15,12 +15,13 @@ use TypeLang\Type\Literal\LiteralNode; use TypeLang\Type\Literal\NullLiteralNode; use TypeLang\Type\Literal\StringLiteralNode; -use TypeLang\Type\Literal\VariableLiteralNode; use TypeLang\Type\NamedTypeNode; use TypeLang\Type\TernaryExpressionNode; +use TypeLang\Type\ThisNode; use TypeLang\Type\TypeOffsetAccessNode; use TypeLang\Type\TypesListNode; use TypeLang\Type\UnionTypeNode; +use TypeLang\Type\WildcardNode; class NativeTypePrinter extends PrettyTypePrinter { @@ -106,7 +107,7 @@ protected function printTypeListNode(TypesListNode $node): string protected function printTernaryType(TernaryExpressionNode $node): string { /** @var non-empty-string */ - return $this->make(new UnionTypeNode($node->then, $node->else)); + return $this->make(new UnionTypeNode([$node->then, $node->else])); } #[\Override] @@ -121,6 +122,18 @@ protected function printConstMaskNode(ConstMaskNode $node): string return 'mixed'; } + #[\Override] + protected function printWildcardNode(WildcardNode $node): string + { + return 'mixed'; + } + + #[\Override] + protected function printThisNode(ThisNode $node): string + { + return 'self'; + } + #[\Override] protected function printClassConstNode(ClassConstNode $node): string { @@ -222,7 +235,6 @@ protected function printLiteralNode(LiteralNode $node): string $node instanceof IntLiteralNode => 'int', $node instanceof NullLiteralNode => 'null', $node instanceof StringLiteralNode => 'string', - $node instanceof VariableLiteralNode => $node->value === 'this' ? 'self' : 'mixed', default => \get_debug_type($node->value), }; } diff --git a/libs/printer/src/PrettyTypePrinter.php b/libs/printer/src/PrettyTypePrinter.php index 7052d56..caf9322 100644 --- a/libs/printer/src/PrettyTypePrinter.php +++ b/libs/printer/src/PrettyTypePrinter.php @@ -4,20 +4,13 @@ namespace TypeLang\Printer; -use TypeLang\Parser\Traverser; use TypeLang\Printer\Exception\NonPrintableNodeException; -use TypeLang\Type\Attribute\AttributeGroupListNode; -use TypeLang\Type\Attribute\AttributeGroupNode; use TypeLang\Type\Callable\CallableParameterNode; use TypeLang\Type\CallableTypeNode; use TypeLang\Type\ClassConstMaskNode; use TypeLang\Type\ClassConstNode; use TypeLang\Type\Condition\Condition; use TypeLang\Type\Condition\EqualConditionNode; -use TypeLang\Type\Condition\GreaterThanConditionNode; -use TypeLang\Type\Condition\GreaterThanOrEqualConditionNode; -use TypeLang\Type\Condition\LessThanConditionNode; -use TypeLang\Type\Condition\LessThanOrEqualConditionNode; use TypeLang\Type\Condition\NotEqualConditionNode; use TypeLang\Type\ConstMaskNode; use TypeLang\Type\IntersectionTypeNode; @@ -26,34 +19,37 @@ use TypeLang\Type\NamedTypeNode; use TypeLang\Type\Node; use TypeLang\Type\NullableTypeNode; -use TypeLang\Type\Shape\ClassConstFieldNode; -use TypeLang\Type\Shape\ClassConstMaskFieldNode; -use TypeLang\Type\Shape\ConstMaskFieldNode; +use TypeLang\Type\Shape\ComplexFieldNode; use TypeLang\Type\Shape\FieldNode; use TypeLang\Type\Shape\FieldsListNode; use TypeLang\Type\Shape\NamedFieldNode; -use TypeLang\Type\Shape\NumericFieldNode; -use TypeLang\Type\Shape\StringNamedFieldNode; +use TypeLang\Type\Shape\ScalarFieldNode; use TypeLang\Type\Template\TemplateArgumentListNode; use TypeLang\Type\Template\TemplateArgumentNode; +use TypeLang\Type\Template\TemplateBoundEdgeNode; +use TypeLang\Type\Template\TemplateParameterListNode; +use TypeLang\Type\Template\TemplateParameterNode; use TypeLang\Type\TernaryExpressionNode; +use TypeLang\Type\ThisNode; use TypeLang\Type\TypeNode; use TypeLang\Type\TypeOffsetAccessNode; use TypeLang\Type\TypesListNode; use TypeLang\Type\UnionTypeNode; +use TypeLang\Type\VariableNode; +use TypeLang\Type\WildcardNode; class PrettyTypePrinter extends TypePrinter { - public const bool DEFAULT_WRAP_INTERSECTION_TYPE = true; + public const DEFAULT_WRAP_INTERSECTION_TYPE = true; - public const bool DEFAULT_WRAP_UNION_TYPE = false; + public const DEFAULT_WRAP_UNION_TYPE = false; - public const bool DEFAULT_WRAP_CALLABLE_RETURN_TYPE = true; + public const DEFAULT_WRAP_CALLABLE_RETURN_TYPE = true; /** * @var int<0, max> */ - public const int DEFAULT_MULTILINE_SHAPE = 1; + public const DEFAULT_MULTILINE_SHAPE = 1; public function __construct( string $newLine = self::DEFAULT_NEW_LINE_DELIMITER, @@ -134,6 +130,8 @@ protected function make(TypeNode $stmt): string $stmt instanceof TernaryExpressionNode => $this->printTernaryType($stmt), $stmt instanceof TypesListNode => $this->printTypeListNode($stmt), $stmt instanceof TypeOffsetAccessNode => $this->printTypeOffsetAccessNode($stmt), + $stmt instanceof WildcardNode => $this->printWildcardNode($stmt), + $stmt instanceof ThisNode => $this->printThisNode($stmt), default => throw NonPrintableNodeException::becauseInvalidNodeGiven($stmt), }; } @@ -195,16 +193,10 @@ private function getShapeFieldsNodes(NamedTypeNode $node, FieldsListNode $shape, $fields = []; foreach ($shape->items as $field) { - $current = ''; - - if ($field->attributes !== null) { - $current .= $this->printAttributeGroups($field->attributes, $multiline); - } - - $fields[] = $current . $prefix . $this->printShapeFieldNode($field); + $fields[] = $prefix . $this->printShapeFieldNode($field); } - if (!$shape->sealed || $node->arguments !== null) { + if (!$shape->isSealed || $node->arguments !== null) { $prefix .= '...'; if ($node->arguments !== null) { @@ -218,35 +210,6 @@ private function getShapeFieldsNodes(NamedTypeNode $node, FieldsListNode $shape, return $fields; } - protected function printAttributeGroups(AttributeGroupListNode $groups, bool $multiline): string - { - $prefix = $this->prefix(); - $result = ''; - - foreach ($groups as $group) { - $result .= $prefix . $this->printAttributeGroup($group); - $result .= $multiline ? $this->newLine : ' '; - } - - return $result; - } - - protected function printAttributeGroup(AttributeGroupNode $group): string - { - $result = '#['; - - $last = $group->last; - foreach ($group as $attribute) { - $result .= $attribute->name->toString(); - - if ($attribute !== $last) { - $result .= ', '; - } - } - - return $result . ']'; - } - /** * @return non-empty-string * @throws NonPrintableNodeException @@ -270,53 +233,39 @@ protected function printShapeFieldNode(FieldNode $field): string return $this->make($field->type); } + /** + * @throws NonPrintableNodeException + */ protected function printShapeFieldName(FieldNode $field): string { return match (true) { - $field instanceof StringNamedFieldNode => $this->printStringShapeFieldName($field), - $field instanceof NumericFieldNode => $this->printNumericShapeFieldName($field), $field instanceof NamedFieldNode => $this->printNamedShapeFieldName($field), - $field instanceof ConstMaskFieldNode => $this->printConstMaskShapeFieldName($field), - $field instanceof ClassConstMaskFieldNode => $this->printClassConstMaskShapeFieldName($field), - $field instanceof ClassConstFieldNode => $this->printClassConstShapeFieldName($field), + $field instanceof ScalarFieldNode => $this->printScalarShapeFieldName($field), + $field instanceof ComplexFieldNode => $this->printComplexShapeFieldName($field), default => $this->printUnknownShapeFieldName($field), }; } - protected function printStringShapeFieldName(StringNamedFieldNode $field): string - { - return $field->key->raw; - } - - protected function printNumericShapeFieldName(NumericFieldNode $field): string - { - return $field->key->raw; - } - protected function printNamedShapeFieldName(NamedFieldNode $field): string { return $field->key->toString(); } - protected function printConstMaskShapeFieldName(ConstMaskFieldNode $field): string - { - return $field->key->name->toString() . '*'; - } - - protected function printClassConstShapeFieldName(ClassConstFieldNode $field): string + /** + * A scalar key is written back the way it was written, so that a "0x2A" + * does not come out as a "42". + */ + protected function printScalarShapeFieldName(ScalarFieldNode $field): string { - return \sprintf('%s::%s', $field->key->class, $field->key->constant); + return $field->key->raw; } - protected function printClassConstMaskShapeFieldName(ClassConstMaskFieldNode $field): string + /** + * @throws NonPrintableNodeException + */ + protected function printComplexShapeFieldName(ComplexFieldNode $field): string { - $constant = $field->key->constant; - - if ($constant === null) { - return \sprintf('%s::*', $field->key->class); - } - - return \sprintf('%s::%s*', $field->key->class, $constant); + return $this->make($field->key); } protected function printUnknownShapeFieldName(FieldNode $field): string @@ -334,13 +283,7 @@ protected function printTemplateArgumentsNode(TemplateArgumentListNode $argument $result = []; foreach ($arguments as $argument) { - $current = ''; - - if ($argument->attributes !== null) { - $current .= $this->printAttributeGroups($argument->attributes, false); - } - - $result[] = $current . $this->printTemplateArgumentNode($argument); + $result[] = $this->printTemplateArgumentNode($argument); } return \sprintf('<%s>', \implode(', ', $result)); @@ -362,6 +305,54 @@ protected function printTemplateArgumentNode(TemplateArgumentNode $argument): st return $result; } + /** + * @param TemplateParameterListNode|TemplateParameterListNode $parameters + * @return non-empty-string + * @throws NonPrintableNodeException + */ + protected function printTemplateParametersNode(TemplateParameterListNode $parameters): string + { + $result = []; + + foreach ($parameters as $parameter) { + $result[] = $this->printTemplateParameterNode($parameter); + } + + return \sprintf('<%s>', \implode(', ', $result)); + } + + /** + * @return non-empty-string + * @throws NonPrintableNodeException + */ + protected function printTemplateParameterNode(TemplateParameterNode $parameter): string + { + $result = $parameter->name->toString(); + + if ($parameter->upper !== null) { + $result .= ' ' . $this->printTemplateBoundEdgeNode($parameter->upper); + } + + if ($parameter->lower !== null) { + $result .= ' ' . $this->printTemplateBoundEdgeNode($parameter->lower); + } + + if ($parameter->default !== null) { + $result .= ' = ' . $this->make($parameter->default); + } + + return $result; + } + + /** + * @return non-empty-string + * @throws NonPrintableNodeException + */ + protected function printTemplateBoundEdgeNode(TemplateBoundEdgeNode $edge): string + { + return \sprintf('%s %s', $edge->operator->toString(), $this->make($edge->type)); + } + /** * @return non-empty-string */ @@ -380,7 +371,7 @@ protected function printClassConstMaskNode(ClassConstMaskNode $node): string { return \vsprintf('%s::%s', [ $node->class->toString(), - (string) $node->constant?->toString() . '*', + $node->mask->toString(), ]); } @@ -389,7 +380,41 @@ protected function printClassConstMaskNode(ClassConstMaskNode $node): string */ protected function printConstMaskNode(ConstMaskNode $node): string { - return $node->name->toString() . '*'; + $result = $node->mask->toString(); + + if ($node->namespace !== null) { + $result = $node->namespace->toUnqualifiedString() . '\\' . $result; + } + + if ($node->isFullyQualified) { + return '\\' . $result; + } + + return $result; + } + + /** + * @return non-empty-string + */ + protected function printThisNode(ThisNode $node): string + { + return '$this'; + } + + /** + * @return non-empty-string + */ + protected function printVariableNode(VariableNode $node): string + { + return '$' . $node->name->toString(); + } + + /** + * @return non-empty-string + */ + protected function printWildcardNode(WildcardNode $node): string + { + return $node->toString(); } /** @@ -400,6 +425,11 @@ protected function printCallableTypeNode(CallableTypeNode $node): string { $result = $node->name->toString(); + // Add template parameters + if ($node->templates !== null) { + $result .= $this->printTemplateParametersNode($node->templates); + } + $arguments = []; foreach ($node->parameters as $argument) { @@ -437,11 +467,6 @@ protected function printCallableArgumentNode(CallableParameterNode $node): strin $result = $this->make($node->type); } - if ($node->attributes !== null) { - $result = $this->printAttributeGroups($node->attributes, false) - . $result; - } - if ($node->name !== null) { $result .= ' '; } @@ -455,8 +480,7 @@ protected function printCallableArgumentNode(CallableParameterNode $node): strin } if ($node->name !== null) { - // @phpstan-ignore-next-line : VariableLiteralNode is a subtype of LiteralNode - $result .= $this->printLiteralNode($node->name); + $result .= $this->printVariableNode($node->name); } if ($node->isOptional) { @@ -472,27 +496,84 @@ protected function shouldWrapReturnType(TypeNode $type): bool return true; } - $visitor = Traverser::through( - visitor: new Traverser\ClassNameMatcherVisitor( - class: LogicalTypeNode::class, - break: static function (Node $node): bool { - // Break on non-empty template parameters. - $isInTemplate = $node instanceof NamedTypeNode - && $node->arguments !== null - && $node->arguments->items !== []; + return self::containsLogicalType($type); + } + + /** + * Searches for a {@see LogicalTypeNode} in depth: The search is stopped + * as soon as a type nesting its children into template arguments or + * shape fields is reached. + */ + private static function containsLogicalType(TypeNode $type): bool + { + $stack = [$type]; + + while ($stack !== []) { + $node = \array_pop($stack); + + if ($node instanceof LogicalTypeNode) { + return true; + } + + if (self::shouldStopSearch($node)) { + return false; + } + + $children = self::fetchChildNodes($node); + + for ($index = \count($children) - 1; $index >= 0; --$index) { + $stack[] = $children[$index]; + } + } + + return false; + } + + private static function shouldStopSearch(Node $node): bool + { + if (!$node instanceof NamedTypeNode) { + return false; + } + + // Stop on non-empty template parameters. + $isInTemplate = $node->arguments !== null + && $node->arguments->items !== []; + + // Stop on non-empty shape fields. + $isInShape = $node->fields !== null + && $node->fields->items !== []; + + return $isInTemplate || $isInShape; + } + + /** + * @return list + */ + private static function fetchChildNodes(Node $node): array + { + $result = []; + + foreach (\get_object_vars($node) as $value) { + if ($value instanceof Node) { + $result[] = $value; + + continue; + } + + if (!\is_iterable($value)) { + continue; + } - // Break on non-empty shape fields. - $isInShape = $node instanceof NamedTypeNode - && $node->fields !== null - && $node->fields->items !== []; + foreach ($value as $child) { + if (!$child instanceof Node) { + break; + } - return $isInTemplate || $isInShape; - }, - ), - nodes: [$type], - ); + $result[] = $child; + } + } - return $visitor->isFound; + return $result; } /** @@ -544,27 +625,34 @@ protected function printNullableType(NullableTypeNode $node): string protected function printTernaryType(TernaryExpressionNode $node): string { return \vsprintf('(%s %s %s ? %s : %s)', [ - $this->make($node->condition->subject), + $this->printConditionOperand($node->condition->subject), $this->printCondition($node->condition), - $this->make($node->condition->target), + $this->printConditionOperand($node->condition->target), $this->make($node->then), $this->make($node->else), ]); } /** - * @return non-empty-string * @throws NonPrintableNodeException */ + protected function printConditionOperand(TypeNode|VariableNode $node): string + { + if ($node instanceof VariableNode) { + return $this->printVariableNode($node); + } + + return $this->make($node); + } + + /** + * @return non-empty-string + */ protected function printCondition(Condition $node): string { return match (true) { $node instanceof EqualConditionNode => 'is', $node instanceof NotEqualConditionNode => 'is not', - $node instanceof GreaterThanOrEqualConditionNode => '>=', - $node instanceof LessThanOrEqualConditionNode => '<=', - $node instanceof GreaterThanConditionNode => '>', - $node instanceof LessThanConditionNode => '<', default => throw NonPrintableNodeException::becauseInvalidNodeGiven($node), }; } diff --git a/libs/printer/src/TypePrinter.php b/libs/printer/src/TypePrinter.php index 02edfc3..8fd7569 100644 --- a/libs/printer/src/TypePrinter.php +++ b/libs/printer/src/TypePrinter.php @@ -12,12 +12,12 @@ abstract class TypePrinter implements TypePrinterInterface /** * @var non-empty-string */ - protected const string DEFAULT_NEW_LINE_DELIMITER = "\n"; + protected const DEFAULT_NEW_LINE_DELIMITER = "\n"; /** * @var non-empty-string */ - protected const string DEFAULT_INDENTION = ' '; + protected const DEFAULT_INDENTION = ' '; /** * @var int<0, max> diff --git a/libs/printer/tests/NativeTypePrinterTest.php b/libs/printer/tests/NativeTypePrinterTest.php index 93127ee..b21e334 100644 --- a/libs/printer/tests/NativeTypePrinterTest.php +++ b/libs/printer/tests/NativeTypePrinterTest.php @@ -15,125 +15,130 @@ final class NativeTypePrinterTest extends TestCase { public function testPrintTypesListAsIterable(): void { - self::assertSame('iterable', new NativeTypePrinter()->print(self::parse('int[]'))); + self::assertSame('iterable', (new NativeTypePrinter())->print(self::parse('int[]'))); } public function testPrintShapeAsPlainName(): void { - self::assertSame('array', new NativeTypePrinter()->print(self::parse('array{foo: int}'))); + self::assertSame('array', (new NativeTypePrinter())->print(self::parse('array{foo: int}'))); } public function testPrintTemplateTypeUsesNameOnly(): void { - self::assertSame('array', new NativeTypePrinter()->print(self::parse('array'))); + self::assertSame('array', (new NativeTypePrinter())->print(self::parse('array'))); } public function testPrintUnionTypeWithoutWhitespaces(): void { - self::assertSame('int|string', new NativeTypePrinter()->print(self::parse('int|string'))); + self::assertSame('int|string', (new NativeTypePrinter())->print(self::parse('int|string'))); } public function testPrintUnionCollapsesMixed(): void { - self::assertSame('mixed', new NativeTypePrinter()->print(self::parse('int|mixed'))); + self::assertSame('mixed', (new NativeTypePrinter())->print(self::parse('int|mixed'))); } public function testPrintUnionCollapsesTrueAndFalseToBool(): void { - self::assertSame('bool', new NativeTypePrinter()->print(self::parse('true|false'))); + self::assertSame('bool', (new NativeTypePrinter())->print(self::parse('true|false'))); } public function testPrintUnionCollapsesBoolPairKeepingOtherMembers(): void { - self::assertSame('bool|int', new NativeTypePrinter()->print(self::parse('true|false|int'))); + self::assertSame('bool|int', (new NativeTypePrinter())->print(self::parse('true|false|int'))); } public function testPrintUnionCollapsesNamedTrueAndFalseToBool(): void { - self::assertSame('bool', new NativeTypePrinter()->print(self::parse('\true|\false'))); + self::assertSame('bool', (new NativeTypePrinter())->print(self::parse('\true|\false'))); } public function testPrintUnionCollapsesNamedBoolPairKeepingOtherMembers(): void { - self::assertSame('int|bool', new NativeTypePrinter()->print(self::parse('\true|\false|int'))); + self::assertSame('int|bool', (new NativeTypePrinter())->print(self::parse('\true|\false|int'))); } public function testPrintUnionDeduplicatesMembers(): void { - self::assertSame('int', new NativeTypePrinter()->print(self::parse('int|int'))); + self::assertSame('int', (new NativeTypePrinter())->print(self::parse('int|int'))); } public function testPrintNestedLogicalTypeIsParenthesized(): void { - self::assertSame('int|(string&Foo)', new NativeTypePrinter() + self::assertSame('int|(string&Foo)', (new NativeTypePrinter()) ->print(self::parse('int|(string&Foo)'))); } public function testPrintDeeplyNestedUnionIsFlattened(): void { - self::assertSame('A|B|C|D', new NativeTypePrinter() + self::assertSame('A|B|C|D', (new NativeTypePrinter()) ->print(self::parse('((A | B) | C) | D'))); } public function testPrintDeeplyNestedIntersectionIsFlattened(): void { - self::assertSame('A&B&C&D', new NativeTypePrinter() + self::assertSame('A&B&C&D', (new NativeTypePrinter()) ->print(self::parse('((A & B) & C) & D'))); } public function testPrintDeeplyNestedMixedLogicalTypeKeepsParentheses(): void { - self::assertSame('((A|B)&C)|D', new NativeTypePrinter() + self::assertSame('((A|B)&C)|D', (new NativeTypePrinter()) ->print(self::parse('((A | B) & C) | D'))); } public function testPrintIntersectionTypeWithoutWhitespaces(): void { - self::assertSame('int&string', new NativeTypePrinter()->print(self::parse('int&string'))); + self::assertSame('int&string', (new NativeTypePrinter())->print(self::parse('int&string'))); } public function testPrintCallableTypeAsName(): void { - self::assertSame('callable', new NativeTypePrinter() + self::assertSame('callable', (new NativeTypePrinter()) ->print(self::parse('callable(int, string): void'))); } public function testPrintTernaryAsUnionOfBranches(): void { - self::assertSame('string|bool', new NativeTypePrinter() + self::assertSame('string|bool', (new NativeTypePrinter()) ->print(self::parse('($x is int ? string : bool)'))); } public function testPrintClassConstantAsMixed(): void { - self::assertSame('mixed', new NativeTypePrinter()->print(self::parse('Foo::BAR'))); + self::assertSame('mixed', (new NativeTypePrinter())->print(self::parse('Foo::BAR'))); } public function testPrintClassConstantMaskAsMixed(): void { - self::assertSame('mixed', new NativeTypePrinter()->print(self::parse('Foo::*'))); + self::assertSame('mixed', (new NativeTypePrinter())->print(self::parse('Foo::*'))); } public function testPrintConstantMaskAsMixed(): void { - self::assertSame('mixed', new NativeTypePrinter()->print(self::parse('FOO_*'))); + self::assertSame('mixed', (new NativeTypePrinter())->print(self::parse('FOO_*'))); + } + + public function testPrintWildcardAsMixed(): void + { + self::assertSame('mixed', (new NativeTypePrinter())->print(new \TypeLang\Type\WildcardNode())); } public function testPrintTypeOffsetAccessAsMixed(): void { - self::assertSame('mixed', new NativeTypePrinter()->print(self::parse('Foo[Bar]'))); + self::assertSame('mixed', (new NativeTypePrinter())->print(self::parse('Foo[Bar]'))); } public function testPrintThisVariableAsSelf(): void { - self::assertSame('self', new NativeTypePrinter() + self::assertSame('self', (new NativeTypePrinter()) ->print(self::parse('($this is int ? $this : $this)'))); } #[DataProvider('literalProvider')] public function testPrintLiteralAsNativeType(string $type, string $expected): void { - self::assertSame($expected, new NativeTypePrinter()->print(self::parse($type))); + self::assertSame($expected, (new NativeTypePrinter())->print(self::parse($type))); } /** @@ -151,7 +156,7 @@ public static function literalProvider(): iterable public function testPrintUsesPreloadedAlias(): void { - self::assertSame('int', new NativeTypePrinter()->print(self::parse('positive-int'))); + self::assertSame('int', (new NativeTypePrinter())->print(self::parse('positive-int'))); } public function testAddTypeAlias(): void @@ -199,6 +204,6 @@ public function testPrintThrowsOnNonPrintableNode(): void $this->expectException(NonPrintableNodeException::class); - new NativeTypePrinter()->print($node); + (new NativeTypePrinter())->print($node); } } diff --git a/libs/printer/tests/PrettyTypePrinterTest.php b/libs/printer/tests/PrettyTypePrinterTest.php index 54412d4..10fcee6 100644 --- a/libs/printer/tests/PrettyTypePrinterTest.php +++ b/libs/printer/tests/PrettyTypePrinterTest.php @@ -113,12 +113,6 @@ public function testPrintTemplateArgumentHint(): void ->print(self::parse('array'))); } - public function testPrintTemplateArgumentAttribute(): void - { - self::assertSame('list<#[Foo] int>', $this->printer() - ->print(self::parse('list<#[Foo] int>'))); - } - public function testPrintInlineShape(): void { self::assertSame('array{foo: int}', $this->printer() @@ -206,20 +200,6 @@ public function testPrintConstantMaskShapeKey(): void ->print(self::parse('array{FOO_*: int}'))); } - public function testPrintShapeFieldAttribute(): void - { - self::assertSame('array{#[Foo] foo: int}', $this->printer() - ->print(self::parse('array{#[Foo] foo: int}'))); - } - - public function testPrintShapeFieldAttributeMultiline(): void - { - self::assertSame( - "array{\n #[Foo]\n foo: int,\n bar: string\n}", - $this->printer()->print(self::parse('array{#[Foo] foo: int, bar: string}')), - ); - } - public function testPrintCallableType(): void { self::assertSame('callable(int, string): void', $this->printer() @@ -256,18 +236,6 @@ public function testPrintOptionalCallableParameter(): void ->print(self::parse('callable(int=): void'))); } - public function testPrintCallableParameterAttribute(): void - { - self::assertSame('callable(#[Foo] int): void', $this->printer() - ->print(self::parse('callable(#[Foo] int): void'))); - } - - public function testPrintCallableParameterMultipleAttributes(): void - { - self::assertSame('callable(#[Foo, Bar] int): void', $this->printer() - ->print(self::parse('callable(#[Foo, Bar] int): void'))); - } - public function testPrintClassConstant(): void { self::assertSame('Foo::BAR', $this->printer()->print(self::parse('Foo::BAR'))); @@ -288,6 +256,54 @@ public function testPrintConstantMask(): void self::assertSame('FOO_*', $this->printer()->print(self::parse('FOO_*'))); } + /** + * @return iterable + */ + public static function maskProvider(): iterable + { + yield 'a wildcard alone' => ['Foo::*']; + yield 'a trailing wildcard' => ['Foo::BAR_*']; + yield 'a leading wildcard' => ['Foo::*_BAR']; + yield 'a wildcard in between' => ['Foo::A*B']; + yield 'several wildcards' => ['Foo::BAR*BAZ*SOME']; + yield 'a global mask' => ['JSON_*_FLAG']; + yield 'a namespaced global mask' => ['Some\Any\JSON_*']; + } + + /** + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('maskProvider')] + public function testPrintMaskOfEverySegmentItIsWrittenOf(string $type): void + { + self::assertSame($type, $this->printer()->print(self::parse($type))); + } + + /** + * @return iterable + */ + public static function wildcardAndTemplateProvider(): iterable + { + yield 'a wildcard argument' => ['Collection<*>']; + yield 'a wildcard beside a type' => ['HashMap']; + yield 'a hinted wildcard' => ['Collection']; + yield 'a template parameter' => ['callable(T): T']; + yield 'a bounded template parameter' => ['callable(T): T']; + yield 'every kind of bound' => ['Closure(T, U): V']; + yield 'several bounds on one parameter' => ['callable(T): void']; + } + + /** + * @param non-empty-string $type + * @throws \Throwable + */ + #[DataProvider('wildcardAndTemplateProvider')] + public function testPrintWildcardsAndTemplateParameters(string $type): void + { + self::assertSame($type, $this->printer()->print(self::parse($type))); + } + public function testPrintTypesList(): void { self::assertSame('int[]', $this->printer()->print(self::parse('int[]'))); @@ -336,10 +352,6 @@ public static function ternaryConditionProvider(): iterable { yield 'equal' => ['($x is int ? string : bool)', '($x is int ? string : bool)']; yield 'not equal' => ['($x is not int ? string : bool)', '($x is not int ? string : bool)']; - yield 'greater than' => ['($x > 5 ? string : bool)', '($x > 5 ? string : bool)']; - yield 'greater than or equal' => ['($x >= 5 ? string : bool)', '($x >= 5 ? string : bool)']; - yield 'less than' => ['($x < 5 ? string : bool)', '($x < 5 ? string : bool)']; - yield 'less than or equal' => ['($x <= 5 ? string : bool)', '($x <= 5 ? string : bool)']; } public function testPrintUsesCustomNewLineAndIndention(): void diff --git a/libs/reader/.github/workflows/tests.yml b/libs/reader/.github/workflows/tests.yml index a7df15d..3abc5cb 100644 --- a/libs/reader/.github/workflows/tests.yml +++ b/libs/reader/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - php: [ '8.4', '8.5' ] + php: [ '8.1', '8.2', '8.3', '8.4', '8.5' ] os: [ ubuntu-latest, macos-latest, windows-latest ] stability: [ lowest, stable ] steps: diff --git a/libs/reader/README.md b/libs/reader/README.md index fa1a977..c5967c3 100644 --- a/libs/reader/README.md +++ b/libs/reader/README.md @@ -32,7 +32,7 @@ composer require type-lang/reader ``` **Requirements:** -- PHP 8.4+ +- PHP 8.1+ ## Usage diff --git a/libs/reader/composer.json b/libs/reader/composer.json index 462002b..7874f25 100644 --- a/libs/reader/composer.json +++ b/libs/reader/composer.json @@ -9,7 +9,7 @@ "issues": "https://github.com/php-type-language/reader/issues" }, "require": { - "php": "^8.4", + "php": "^8.1", "type-lang/types": "^2.0" }, "autoload": { @@ -18,8 +18,8 @@ } }, "require-dev": { - "phpstan/phpstan": "^2.2.5", - "phpunit/phpunit": "^13.2.4", + "phpstan/phpstan": "^2.2", + "phpunit/phpunit": "^10.5|^11.0|^12.0|^13.0", "type-lang/printer": "^2.0" }, "autoload-dev": { diff --git a/libs/reader/phpstan.neon b/libs/reader/phpstan.neon index 018e46e..f2d4b90 100644 --- a/libs/reader/phpstan.neon +++ b/libs/reader/phpstan.neon @@ -1,5 +1,9 @@ parameters: level: max + phpVersion: + min: 80100 + max: 80599 + treatPhpDocTypesAsCertain: false fileExtensions: - php paths: diff --git a/libs/reader/src/PropertyAccessDirection.php b/libs/reader/src/PropertyAccessDirection.php index a1a8e71..f090435 100644 --- a/libs/reader/src/PropertyAccessDirection.php +++ b/libs/reader/src/PropertyAccessDirection.php @@ -9,5 +9,5 @@ enum PropertyAccessDirection case Read; case Write; - public const self DEFAULT = self::Read; + public const DEFAULT = self::Read; } diff --git a/libs/reader/src/ReflectionReader.php b/libs/reader/src/ReflectionReader.php index 3818a85..695a510 100644 --- a/libs/reader/src/ReflectionReader.php +++ b/libs/reader/src/ReflectionReader.php @@ -23,10 +23,15 @@ final class ReflectionReader implements ReaderInterface /** * @var list */ - private const array NULLABLE_BUILTIN_TYPES = ['null', 'mixed']; + private const NULLABLE_BUILTIN_TYPES = ['null', 'mixed']; public function findConstantType(\ReflectionClassConstant $constant): ?TypeNode { + // Constant types are available since PHP 8.3 + if (!\method_exists($constant, 'getType')) { + return null; + } + $type = $constant->getType(); if ($type instanceof \ReflectionType) { @@ -47,7 +52,13 @@ private function findPropertyNativeReadType(\ReflectionProperty $property): ?\Re private function findPropertyNativeWriteType(\ReflectionProperty $property): ?\ReflectionType { - $setter = $property->getHook(\PropertyHookType::Set); + // Property hooks are available since PHP 8.4 + if (!\method_exists($property, 'getHook')) { + return $this->findPropertyNativeReadType($property); + } + + /** @var \ReflectionMethod|null $setter */ + $setter = $property->getHook(\constant('PropertyHookType::Set')); if ($setter === null) { return $this->findPropertyNativeReadType($property); @@ -149,7 +160,7 @@ private function convertNonNullNamedType(\ReflectionNamedType $type): TypeNode $name = Name::createFromString($literal); - if ($type->isBuiltin() || $name->isSpecial || $name->isBuiltin) { + if ($type->isBuiltin() || $name->isSpecial() || $name->isBuiltin()) { return new NamedTypeNode($name); } @@ -168,7 +179,7 @@ private function convertUnionType(\ReflectionUnionType $type): UnionTypeNode $children[] = $this->getType($child); } - return new UnionTypeNode(...$children); + return new UnionTypeNode($children); } /** @@ -183,6 +194,6 @@ private function convertIntersectionType(\ReflectionIntersectionType $type): Int $children[] = $this->getType($child); } - return new IntersectionTypeNode(...$children); + return new IntersectionTypeNode($children); } } diff --git a/libs/reader/tests/ConstantReaderTest.php b/libs/reader/tests/ConstantReaderTest.php index abe8843..c9fe565 100644 --- a/libs/reader/tests/ConstantReaderTest.php +++ b/libs/reader/tests/ConstantReaderTest.php @@ -6,9 +6,11 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresPhp; use TypeLang\Reader\ConstantReaderInterface; use TypeLang\Reader\Tests\Stub\__ConstantReaderEnum; use TypeLang\Reader\Tests\Stub\ConstantReaderStub; +use TypeLang\Reader\Tests\Stub\ConstantReaderStub83; use TypeLang\Type\IntersectionTypeNode; use TypeLang\Type\NullableTypeNode; use TypeLang\Type\UnionTypeNode; @@ -17,62 +19,67 @@ class ConstantReaderTest extends ReaderTestCase { #[DataProvider('readersDataProvider')] + #[RequiresPhp('>= 8.3')] public function testSimpleType(ConstantReaderInterface $reader): void { $type = $reader->findConstantType( - constant: new \ReflectionClassConstant(ConstantReaderStub::class, 'SINGLE'), + constant: new \ReflectionClassConstant(ConstantReaderStub83::class, 'SINGLE'), ); self::assertSameType(self::builtin('int'), $type); } #[DataProvider('readersDataProvider')] + #[RequiresPhp('>= 8.3')] public function testUnionType(ConstantReaderInterface $reader): void { $type = $reader->findConstantType( - constant: new \ReflectionClassConstant(ConstantReaderStub::class, 'UNION'), + constant: new \ReflectionClassConstant(ConstantReaderStub83::class, 'UNION'), ); - self::assertSameType(new UnionTypeNode( + self::assertSameType(new UnionTypeNode([ self::builtin('string'), - self::builtin('int'), - ), $type); + self::builtin('int') + ]), $type); } #[DataProvider('readersDataProvider')] + #[RequiresPhp('>= 8.3')] public function testIntersectionType(ConstantReaderInterface $reader): void { $type = $reader->findConstantType( - constant: new \ReflectionClassConstant(ConstantReaderStub::class, 'INTERSECTION'), + constant: new \ReflectionClassConstant(ConstantReaderStub83::class, 'INTERSECTION'), ); - self::assertSameType(new IntersectionTypeNode( + self::assertSameType(new IntersectionTypeNode([ self::classType(__ConstantReaderEnum::class), - self::classType(\BackedEnum::class), - ), $type); + self::classType(\BackedEnum::class) + ]), $type); } #[DataProvider('readersDataProvider')] + #[RequiresPhp('>= 8.3')] public function testCompositeType(ConstantReaderInterface $reader): void { $type = $reader->findConstantType( - constant: new \ReflectionClassConstant(ConstantReaderStub::class, 'COMPOSITE'), + constant: new \ReflectionClassConstant(ConstantReaderStub83::class, 'COMPOSITE'), ); - self::assertSameType(new UnionTypeNode( - new IntersectionTypeNode( + self::assertSameType(new UnionTypeNode([ + new IntersectionTypeNode([ self::classType(__ConstantReaderEnum::class), - self::classType(\BackedEnum::class), - ), - self::builtin('array'), - ), $type); + self::classType(\BackedEnum::class) + ]), + self::builtin('array') + ]), $type); } #[DataProvider('readersDataProvider')] + #[RequiresPhp('>= 8.3')] public function testNullableType(ConstantReaderInterface $reader): void { $type = $reader->findConstantType( - constant: new \ReflectionClassConstant(ConstantReaderStub::class, 'NULLABLE'), + constant: new \ReflectionClassConstant(ConstantReaderStub83::class, 'NULLABLE'), ); self::assertSameType(new NullableTypeNode(self::builtin('int')), $type); diff --git a/libs/reader/tests/FunctionReaderTest.php b/libs/reader/tests/FunctionReaderTest.php index 0790311..8cf9b85 100644 --- a/libs/reader/tests/FunctionReaderTest.php +++ b/libs/reader/tests/FunctionReaderTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresPhp; use TypeLang\Reader\FunctionReaderInterface; use TypeLang\Type\IntersectionTypeNode; use TypeLang\Type\NullableTypeNode; @@ -36,10 +37,10 @@ public function testUnionType(FunctionReaderInterface $reader): void function: new \ReflectionFunction('TypeLang\Reader\Tests\Stub\get_union_type'), ); - self::assertSameType(new UnionTypeNode( + self::assertSameType(new UnionTypeNode([ self::builtin('string'), - self::builtin('int'), - ), $type); + self::builtin('int') + ]), $type); } #[DataProvider('readersDataProvider')] @@ -49,26 +50,29 @@ public function testIntersectionType(FunctionReaderInterface $reader): void function: new \ReflectionFunction('TypeLang\Reader\Tests\Stub\get_intersection_type'), ); - self::assertSameType(new IntersectionTypeNode( + self::assertSameType(new IntersectionTypeNode([ self::classType(\ArrayAccess::class), - self::classType(\Traversable::class), - ), $type); + self::classType(\Traversable::class) + ]), $type); } #[DataProvider('readersDataProvider')] + #[RequiresPhp('>= 8.2')] public function testCompositeType(FunctionReaderInterface $reader): void { + require_once __DIR__ . '/Stub/functions_reader_stub_82.php'; + $type = $reader->findFunctionType( function: new \ReflectionFunction('TypeLang\Reader\Tests\Stub\get_composite_type'), ); - self::assertSameType(new UnionTypeNode( - new IntersectionTypeNode( + self::assertSameType(new UnionTypeNode([ + new IntersectionTypeNode([ self::classType(\ArrayAccess::class), - self::classType(\Traversable::class), - ), - self::builtin('array'), - ), $type); + self::classType(\Traversable::class) + ]), + self::builtin('array') + ]), $type); } #[DataProvider('readersDataProvider')] diff --git a/libs/reader/tests/MethodReaderTest.php b/libs/reader/tests/MethodReaderTest.php index d975729..e2955be 100644 --- a/libs/reader/tests/MethodReaderTest.php +++ b/libs/reader/tests/MethodReaderTest.php @@ -6,8 +6,10 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresPhp; use TypeLang\Reader\FunctionReaderInterface; use TypeLang\Reader\Tests\Stub\MethodReaderStub; +use TypeLang\Reader\Tests\Stub\MethodReaderStub82; use TypeLang\Type\IntersectionTypeNode; use TypeLang\Type\UnionTypeNode; @@ -31,10 +33,10 @@ public function testUnionType(FunctionReaderInterface $reader): void function: new \ReflectionMethod(MethodReaderStub::class, 'getUnionType'), ); - self::assertSameType(new UnionTypeNode( + self::assertSameType(new UnionTypeNode([ self::builtin('string'), - self::builtin('int'), - ), $type); + self::builtin('int') + ]), $type); } #[DataProvider('readersDataProvider')] @@ -44,25 +46,26 @@ public function testIntersectionType(FunctionReaderInterface $reader): void function: new \ReflectionMethod(MethodReaderStub::class, 'getIntersectionType'), ); - self::assertSameType(new IntersectionTypeNode( + self::assertSameType(new IntersectionTypeNode([ self::classType(\ArrayAccess::class), - self::classType(\Traversable::class), - ), $type); + self::classType(\Traversable::class) + ]), $type); } #[DataProvider('readersDataProvider')] + #[RequiresPhp('>= 8.2')] public function testCompositeType(FunctionReaderInterface $reader): void { $type = $reader->findFunctionType( - function: new \ReflectionMethod(MethodReaderStub::class, 'getCompositeType'), + function: new \ReflectionMethod(MethodReaderStub82::class, 'getCompositeType'), ); - self::assertSameType(new UnionTypeNode( - new IntersectionTypeNode( + self::assertSameType(new UnionTypeNode([ + new IntersectionTypeNode([ self::classType(\ArrayAccess::class), - self::classType(\Traversable::class), - ), - self::builtin('array'), - ), $type); + self::classType(\Traversable::class) + ]), + self::builtin('array') + ]), $type); } } diff --git a/libs/reader/tests/ParameterReaderTest.php b/libs/reader/tests/ParameterReaderTest.php index 49cbc2e..6361a33 100644 --- a/libs/reader/tests/ParameterReaderTest.php +++ b/libs/reader/tests/ParameterReaderTest.php @@ -6,8 +6,10 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresPhp; use TypeLang\Reader\ParameterReaderInterface; use TypeLang\Reader\Tests\Stub\ParameterReaderStub; +use TypeLang\Reader\Tests\Stub\ParameterReaderStub82; use TypeLang\Type\IntersectionTypeNode; use TypeLang\Type\NullableTypeNode; use TypeLang\Type\UnionTypeNode; @@ -32,10 +34,10 @@ public function testUnionType(ParameterReaderInterface $reader): void parameter: new \ReflectionParameter(ParameterReaderStub::withUnionType(...), 0), ); - self::assertSameType(new UnionTypeNode( + self::assertSameType(new UnionTypeNode([ self::builtin('string'), - self::builtin('int'), - ), $type); + self::builtin('int') + ]), $type); } #[DataProvider('readersDataProvider')] @@ -45,26 +47,27 @@ public function testIntersectionType(ParameterReaderInterface $reader): void parameter: new \ReflectionParameter(ParameterReaderStub::withIntersectionType(...), 0), ); - self::assertSameType(new IntersectionTypeNode( + self::assertSameType(new IntersectionTypeNode([ self::classType(\ArrayAccess::class), - self::classType(\Traversable::class), - ), $type); + self::classType(\Traversable::class) + ]), $type); } #[DataProvider('readersDataProvider')] + #[RequiresPhp('>= 8.2')] public function testCompositeType(ParameterReaderInterface $reader): void { $type = $reader->findParameterType( - parameter: new \ReflectionParameter(ParameterReaderStub::withCompositeType(...), 0), + parameter: new \ReflectionParameter(ParameterReaderStub82::withCompositeType(...), 0), ); - self::assertSameType(new UnionTypeNode( - new IntersectionTypeNode( + self::assertSameType(new UnionTypeNode([ + new IntersectionTypeNode([ self::classType(\ArrayAccess::class), - self::classType(\Traversable::class), - ), - self::builtin('array'), - ), $type); + self::classType(\Traversable::class) + ]), + self::builtin('array') + ]), $type); } #[DataProvider('readersDataProvider')] diff --git a/libs/reader/tests/PropertyReaderTest.php b/libs/reader/tests/PropertyReaderTest.php index 2fc0ae7..d249ea4 100644 --- a/libs/reader/tests/PropertyReaderTest.php +++ b/libs/reader/tests/PropertyReaderTest.php @@ -6,9 +6,12 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresPhp; use TypeLang\Reader\PropertyAccessDirection; use TypeLang\Reader\PropertyReaderInterface; use TypeLang\Reader\Tests\Stub\PropertyReaderStub; +use TypeLang\Reader\Tests\Stub\PropertyReaderStub82; +use TypeLang\Reader\Tests\Stub\PropertyReaderStub84; use TypeLang\Type\IntersectionTypeNode; use TypeLang\Type\NullableTypeNode; use TypeLang\Type\UnionTypeNode; @@ -33,10 +36,10 @@ public function testUnionType(PropertyReaderInterface $reader): void property: new \ReflectionProperty(PropertyReaderStub::class, 'unionType'), ); - self::assertSameType(new UnionTypeNode( + self::assertSameType(new UnionTypeNode([ self::builtin('string'), - self::builtin('int'), - ), $type); + self::builtin('int') + ]), $type); } #[DataProvider('readersDataProvider')] @@ -46,26 +49,27 @@ public function testIntersectionType(PropertyReaderInterface $reader): void property: new \ReflectionProperty(PropertyReaderStub::class, 'intersectionType'), ); - self::assertSameType(new IntersectionTypeNode( + self::assertSameType(new IntersectionTypeNode([ self::classType(\ArrayAccess::class), - self::classType(\Traversable::class), - ), $type); + self::classType(\Traversable::class) + ]), $type); } #[DataProvider('readersDataProvider')] + #[RequiresPhp('>= 8.2')] public function testCompositeType(PropertyReaderInterface $reader): void { $type = $reader->findPropertyType( - property: new \ReflectionProperty(PropertyReaderStub::class, 'compositeType'), + property: new \ReflectionProperty(PropertyReaderStub82::class, 'compositeType'), ); - self::assertSameType(new UnionTypeNode( - new IntersectionTypeNode( + self::assertSameType(new UnionTypeNode([ + new IntersectionTypeNode([ self::classType(\ArrayAccess::class), - self::classType(\Traversable::class), - ), - self::builtin('array'), - ), $type); + self::classType(\Traversable::class) + ]), + self::builtin('array') + ]), $type); } #[DataProvider('readersDataProvider')] @@ -89,10 +93,11 @@ public function testUntypedPropertyHasNoType(PropertyReaderInterface $reader): v } #[DataProvider('readersDataProvider')] + #[RequiresPhp('>= 8.4')] public function testHookedPropertyReadType(PropertyReaderInterface $reader): void { $type = $reader->findPropertyType( - property: new \ReflectionProperty(PropertyReaderStub::class, 'hookedType'), + property: new \ReflectionProperty(PropertyReaderStub84::class, 'hookedType'), access: PropertyAccessDirection::Read, ); @@ -100,18 +105,19 @@ public function testHookedPropertyReadType(PropertyReaderInterface $reader): voi } #[DataProvider('readersDataProvider')] + #[RequiresPhp('>= 8.4')] public function testHookedPropertyWriteType(PropertyReaderInterface $reader): void { $type = $reader->findPropertyType( - property: new \ReflectionProperty(PropertyReaderStub::class, 'hookedType'), + property: new \ReflectionProperty(PropertyReaderStub84::class, 'hookedType'), access: PropertyAccessDirection::Write, ); // The "set" hook widens the accepted type to "string|\Stringable". - self::assertSameType(new UnionTypeNode( + self::assertSameType(new UnionTypeNode([ self::classType(\Stringable::class), - self::builtin('string'), - ), $type); + self::builtin('string') + ]), $type); } #[DataProvider('readersDataProvider')] diff --git a/libs/reader/tests/ReflectionReaderTest.php b/libs/reader/tests/ReflectionReaderTest.php index 09c69f5..960892b 100644 --- a/libs/reader/tests/ReflectionReaderTest.php +++ b/libs/reader/tests/ReflectionReaderTest.php @@ -5,11 +5,12 @@ namespace TypeLang\Reader\Tests; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresPhp; use TypeLang\Reader\Exception\UnrecognizedConstantTypeException; use TypeLang\Reader\Exception\UnrecognizedReflectionTypeException; use TypeLang\Reader\Exception\UnrecognizedTypeException; use TypeLang\Reader\ReflectionReader; -use TypeLang\Reader\Tests\Stub\ConstantReaderStub; +use TypeLang\Reader\Tests\Stub\ConstantReaderStub83; #[Group('type-lang/reader')] class ReflectionReaderTest extends ReaderTestCase @@ -19,9 +20,9 @@ public function testGetTypeConvertsNamedType(): void $reader = new ReflectionReader(); $type = $reader->getType( - new \ReflectionProperty(new class { + (new \ReflectionProperty(new class { public int $value; - }, 'value')->getType(), + }, 'value'))->getType(), ); self::assertSameType(self::builtin('int'), $type); @@ -50,7 +51,7 @@ public function __toString(): string self::fail('Expected an UnrecognizedTypeException to be thrown'); } catch (UnrecognizedTypeException $e) { self::assertInstanceOf(UnrecognizedReflectionTypeException::class, $e); - self::assertTrue(new \ReflectionClass($e)->isFinal()); + self::assertTrue((new \ReflectionClass($e))->isFinal()); } } @@ -67,19 +68,20 @@ public function testMixedTypeIsNotNullable(): void $reader = new ReflectionReader(); $type = $reader->getType( - new \ReflectionProperty(new class { + (new \ReflectionProperty(new class { public mixed $value; - }, 'value')->getType(), + }, 'value'))->getType(), ); self::assertSameType(self::builtin('mixed'), $type); } + #[RequiresPhp('>= 8.3')] public function testContextualExceptionWrapsAndChainsRootCause(): void { $reader = new ReflectionReader(); - $constant = new \ReflectionClassConstant(ConstantReaderStub::class, 'SINGLE'); + $constant = new \ReflectionClassConstant(ConstantReaderStub83::class, 'SINGLE'); $previous = UnrecognizedTypeException::becauseTypeIsUnrecognized($constant->getType()); $exception = UnrecognizedConstantTypeException::becauseConstantTypeIsUnrecognized( @@ -89,7 +91,7 @@ public function testContextualExceptionWrapsAndChainsRootCause(): void ); self::assertInstanceOf(UnrecognizedReflectionTypeException::class, $exception); - self::assertTrue(new \ReflectionClass($exception)->isFinal()); + self::assertTrue((new \ReflectionClass($exception))->isFinal()); self::assertSame($previous, $exception->getPrevious()); self::assertStringContainsString('SINGLE', $exception->getMessage()); } diff --git a/libs/reader/tests/StaticMethodReaderTest.php b/libs/reader/tests/StaticMethodReaderTest.php index 036841d..52acddd 100644 --- a/libs/reader/tests/StaticMethodReaderTest.php +++ b/libs/reader/tests/StaticMethodReaderTest.php @@ -6,8 +6,10 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresPhp; use TypeLang\Reader\FunctionReaderInterface; use TypeLang\Reader\Tests\Stub\StaticMethodReaderStub; +use TypeLang\Reader\Tests\Stub\StaticMethodReaderStub82; use TypeLang\Type\IntersectionTypeNode; use TypeLang\Type\UnionTypeNode; @@ -31,10 +33,10 @@ public function testUnionType(FunctionReaderInterface $reader): void function: new \ReflectionMethod(StaticMethodReaderStub::class, 'getUnionType'), ); - self::assertSameType(new UnionTypeNode( + self::assertSameType(new UnionTypeNode([ self::builtin('string'), - self::builtin('int'), - ), $type); + self::builtin('int') + ]), $type); } #[DataProvider('readersDataProvider')] @@ -44,25 +46,26 @@ public function testIntersectionType(FunctionReaderInterface $reader): void function: new \ReflectionMethod(StaticMethodReaderStub::class, 'getIntersectionType'), ); - self::assertSameType(new IntersectionTypeNode( + self::assertSameType(new IntersectionTypeNode([ self::classType(\ArrayAccess::class), - self::classType(\Traversable::class), - ), $type); + self::classType(\Traversable::class) + ]), $type); } #[DataProvider('readersDataProvider')] + #[RequiresPhp('>= 8.2')] public function testCompositeType(FunctionReaderInterface $reader): void { $type = $reader->findFunctionType( - function: new \ReflectionMethod(StaticMethodReaderStub::class, 'getCompositeType'), + function: new \ReflectionMethod(StaticMethodReaderStub82::class, 'getCompositeType'), ); - self::assertSameType(new UnionTypeNode( - new IntersectionTypeNode( + self::assertSameType(new UnionTypeNode([ + new IntersectionTypeNode([ self::classType(\ArrayAccess::class), - self::classType(\Traversable::class), - ), - self::builtin('array'), - ), $type); + self::classType(\Traversable::class) + ]), + self::builtin('array') + ]), $type); } } diff --git a/libs/reader/tests/Stub/ConstantReaderStub.php b/libs/reader/tests/Stub/ConstantReaderStub.php index 258011b..87fa941 100644 --- a/libs/reader/tests/Stub/ConstantReaderStub.php +++ b/libs/reader/tests/Stub/ConstantReaderStub.php @@ -4,22 +4,7 @@ namespace TypeLang\Reader\Tests\Stub; -enum __ConstantReaderEnum: int -{ - case EXAMPLE = 0; -} - final class ConstantReaderStub { - public const int SINGLE = 0xDEAD_BEEF; - - public const int|string UNION = 0xDEAD_BEEF; - - public const __ConstantReaderEnum&\BackedEnum INTERSECTION = __ConstantReaderEnum::EXAMPLE; - - public const (__ConstantReaderEnum&\BackedEnum)|array COMPOSITE = __ConstantReaderEnum::EXAMPLE; - - public const ?int NULLABLE = null; - public const UNTYPED = 0xDEAD_BEEF; } diff --git a/libs/reader/tests/Stub/ConstantReaderStub83.php b/libs/reader/tests/Stub/ConstantReaderStub83.php new file mode 100644 index 0000000..d1867ae --- /dev/null +++ b/libs/reader/tests/Stub/ConstantReaderStub83.php @@ -0,0 +1,18 @@ + $this->hookedType; - set(string|\Stringable $value) => (string) $value; - } } diff --git a/libs/reader/tests/Stub/PropertyReaderStub82.php b/libs/reader/tests/Stub/PropertyReaderStub82.php new file mode 100644 index 0000000..27d0d71 --- /dev/null +++ b/libs/reader/tests/Stub/PropertyReaderStub82.php @@ -0,0 +1,10 @@ + $this->hookedType; + set(string|\Stringable $value) => (string) $value; + } +} diff --git a/libs/reader/tests/Stub/StaticMethodReaderStub.php b/libs/reader/tests/Stub/StaticMethodReaderStub.php index bd59b7c..67a5907 100644 --- a/libs/reader/tests/Stub/StaticMethodReaderStub.php +++ b/libs/reader/tests/Stub/StaticMethodReaderStub.php @@ -11,6 +11,4 @@ public static function getSingleType(): int; public static function getUnionType(): int|string; public static function getIntersectionType(): \ArrayAccess&\Traversable; - - public static function getCompositeType(): (\ArrayAccess&\Traversable)|array; } diff --git a/libs/reader/tests/Stub/StaticMethodReaderStub82.php b/libs/reader/tests/Stub/StaticMethodReaderStub82.php new file mode 100644 index 0000000..4421978 --- /dev/null +++ b/libs/reader/tests/Stub/StaticMethodReaderStub82.php @@ -0,0 +1,10 @@ + - */ -final class AttributeArgumentListNode extends NodeList {} diff --git a/libs/types/src/Attribute/AttributeArgumentNode.php b/libs/types/src/Attribute/AttributeArgumentNode.php deleted file mode 100644 index 5c23012..0000000 --- a/libs/types/src/Attribute/AttributeArgumentNode.php +++ /dev/null @@ -1,16 +0,0 @@ - - */ -final class AttributeGroupListNode extends NodeList {} diff --git a/libs/types/src/Attribute/AttributeGroupNode.php b/libs/types/src/Attribute/AttributeGroupNode.php deleted file mode 100644 index 04a38c4..0000000 --- a/libs/types/src/Attribute/AttributeGroupNode.php +++ /dev/null @@ -1,12 +0,0 @@ - - */ -final class AttributeGroupNode extends NodeList {} diff --git a/libs/types/src/Attribute/AttributeNode.php b/libs/types/src/Attribute/AttributeNode.php deleted file mode 100644 index 99f1110..0000000 --- a/libs/types/src/Attribute/AttributeNode.php +++ /dev/null @@ -1,16 +0,0 @@ - */ final class CallableParameterListNode extends NodeList {} diff --git a/libs/types/src/Callable/CallableParameterNode.php b/libs/types/src/Callable/CallableParameterNode.php index aa1e23a..d147876 100644 --- a/libs/types/src/Callable/CallableParameterNode.php +++ b/libs/types/src/Callable/CallableParameterNode.php @@ -4,21 +4,63 @@ namespace TypeLang\Type\Callable; -use TypeLang\Type\Attribute\AttributeGroupListNode; -use TypeLang\Type\Literal\VariableLiteralNode; use TypeLang\Type\Node; use TypeLang\Type\TypeNode; +use TypeLang\Type\VariableNode; -final class CallableParameterNode extends Node implements \Stringable +/** + * A single parameter of a callable. + * + * ``` + * callable(int &$byRef, string ...$rest, bool $flag = ) + * // ^^^^^^^^^^^ ^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^ + * // by reference variadic optional + * ``` + */ +final class CallableParameterNode extends Node { + /** + * @param int<0, max> $offset + */ public function __construct( + /** + * Type of parameter. A statement always writes one, so a + * {@see null} only ever reaches a tree that is built by hand, where + * a name alone is enough to tell one parameter from another. + */ public ?TypeNode $type = null, - public ?VariableLiteralNode $name = null, + /** + * Name of a parameter, or {@see null} in case of it is written + * without one. + * + * ``` + * callable(int) // null + * callable(int $value) // a name + * ``` + */ + public ?VariableNode $name = null, + /** + * Whether a parameter is taken by reference. + * + * It is written as an `&` behind the type, like the one of + * a `callable(int &$byRef)`. + */ public bool $isOutput = false, + /** + * Whether a parameter takes every argument left, written as a `...`: + * A `callable(string ...$rest)`. + */ public bool $isVariadic = false, + /** + * Whether a parameter may be left out, written as a trailing `=`: + * A `callable(bool $flag =)`. A variadic one is optional already, so + * the two are never both set. + */ public bool $isOptional = false, - public ?AttributeGroupListNode $attributes = null, + int $offset = 0, ) { + parent::__construct($offset); + \assert($type !== null || $name !== null, new \TypeError( 'Required indication of the type or name of the parameter (one of)', )); @@ -28,26 +70,14 @@ public function __construct( )); } - public function __toString(): string + /** + * Returns {@see true} in case of the parameter is an instance of the + * passed class. + * + * @param class-string $class + */ + public function is(string $class): bool { - $result = []; - - if ($this->isOutput) { - $result[] = 'output'; - } - - if ($this->isVariadic) { - $result[] = 'variadic'; - } - - if ($this->isOptional) { - $result[] = 'optional'; - } - - if ($result === []) { - return 'simple'; - } - - return \implode(', ', $result); + return $this instanceof $class; } } diff --git a/libs/types/src/CallableTypeNode.php b/libs/types/src/CallableTypeNode.php index 4d3b817..ac31b2d 100644 --- a/libs/types/src/CallableTypeNode.php +++ b/libs/types/src/CallableTypeNode.php @@ -5,12 +5,61 @@ namespace TypeLang\Type; use TypeLang\Type\Callable\CallableParameterListNode; +use TypeLang\Type\Template\TemplateParameterListNode; +/** + * Something callable, written as a name with a parameter list behind it. + * + * ``` + * Closure(T, string ...$rest): T + * ^^^^^^^ the name + * ^^^^^^^^^^^ the template parameters + * ^^^^^^^^^^^^^^^^^^^^ the parameters + * ^ the return type + * ``` + */ final class CallableTypeNode extends TypeNode { + /** + * @param int<0, max> $offset + */ public function __construct( public Name $name, + /** + * Parameters a callable accepts. Unlike the ones below, the list is + * never a {@see null}: A callable is written with its parentheses, + * and an empty list is what stands inside an empty pair of them. + * + * ``` + * callable() // an empty list + * callable(int) // a list of 1 parameter + * ``` + */ public CallableParameterListNode $parameters = new CallableParameterListNode(), + /** + * A return type of callable, or {@see null} in case of it is left + * unsaid. + * + * ``` + * callable() // null + * callable(): int // a type + * ``` + */ public ?TypeNode $type = null, - ) {} + /** + * Template parameters a callable declares, or {@see null} in case of + * it declares none. + * + * The list is never empty, since a `callable<>()` is no type. + * + * ``` + * callable(T): T // null + * callable(T): T // a list of 1 parameter + * ``` + */ + public ?TemplateParameterListNode $templates = null, + int $offset = 0, + ) { + parent::__construct($offset); + } } diff --git a/libs/types/src/ClassConstMaskNode.php b/libs/types/src/ClassConstMaskNode.php index a9332fd..50261ce 100644 --- a/libs/types/src/ClassConstMaskNode.php +++ b/libs/types/src/ClassConstMaskNode.php @@ -4,10 +4,25 @@ namespace TypeLang\Type; +/** + * A class constant referenced by a name written in part. + * + * ``` + * Some\Any::CONST_* + * ^^^^^^^^ the class the constant belongs to + * ^^^^^^^ the mask its name is written as + * ``` + */ final class ClassConstMaskNode extends TypeNode { + /** + * @param int<0, max> $offset + */ public function __construct( public Name $class, - public ?Identifier $constant = null, - ) {} + public MaskNode $mask = new MaskNode([new WildcardNode()]), + int $offset = 0, + ) { + parent::__construct($offset); + } } diff --git a/libs/types/src/ClassConstNode.php b/libs/types/src/ClassConstNode.php index f86f5f1..04e98a1 100644 --- a/libs/types/src/ClassConstNode.php +++ b/libs/types/src/ClassConstNode.php @@ -4,10 +4,27 @@ namespace TypeLang\Type; +/** + * A constant of a class, referenced by its whole name. + * + * ``` + * Some\Any::CONST_NAME + * ^^^^^^^^ the class the constant belongs to + * ^^^^^^^^^^ the name of the constant + * ``` + * + * A name written in part is a {@see ClassConstMaskNode} instead. + */ final class ClassConstNode extends TypeNode { + /** + * @param int<0, max> $offset + */ public function __construct( public Name $class, public Identifier $constant, - ) {} + int $offset = 0, + ) { + parent::__construct($offset); + } } diff --git a/libs/types/src/Condition/Condition.php b/libs/types/src/Condition/Condition.php index 1150483..948835b 100644 --- a/libs/types/src/Condition/Condition.php +++ b/libs/types/src/Condition/Condition.php @@ -6,11 +6,28 @@ use TypeLang\Type\Node; use TypeLang\Type\TypeNode; +use TypeLang\Type\VariableNode; +/** + * The question a ternary type asks, that is, the two sides of it and, + * in the class itself, the operator between them. + * + * ``` + * ($value is int ? string : bool) + * //^^^^^^ ^^ ^^^ + * //subject target + * ``` + */ abstract class Condition extends Node { + /** + * @param int<0, max> $offset + */ public function __construct( - public TypeNode $subject, - public TypeNode $target, - ) {} + public TypeNode|VariableNode $subject, + public TypeNode|VariableNode $target, + int $offset = 0, + ) { + parent::__construct($offset); + } } diff --git a/libs/types/src/Condition/EqualConditionNode.php b/libs/types/src/Condition/EqualConditionNode.php index 850de39..ac9a7a9 100644 --- a/libs/types/src/Condition/EqualConditionNode.php +++ b/libs/types/src/Condition/EqualConditionNode.php @@ -4,4 +4,11 @@ namespace TypeLang\Type\Condition; +/** + * Whether the subject is the target. + * + * ``` + * ($value is int ? string : bool) + * ``` + */ final class EqualConditionNode extends Condition {} diff --git a/libs/types/src/Condition/GreaterThanConditionNode.php b/libs/types/src/Condition/GreaterThanConditionNode.php deleted file mode 100644 index 5826ae6..0000000 --- a/libs/types/src/Condition/GreaterThanConditionNode.php +++ /dev/null @@ -1,7 +0,0 @@ - $offset + */ public function __construct( - public Name $name, - ) {} + /** + * The name of the constant, written in part. + */ + public MaskNode $mask, + /** + * The namespace the constant belongs to, the leading separator of which + * says whether the reference is fully qualified. + * + * A {@see bool} stands for that separator alone, in case of the + * constant is written with no namespace. + */ + public Name|bool $namespaceOrFullyQualified = Name::IS_FULLY_QUALIFIED_DEFAULT_VALUE, + int $offset = 0, + ) { + parent::__construct($offset); + } + + public function getNamespace(): ?Name + { + $context = $this->namespaceOrFullyQualified; + + if ($context instanceof Name) { + return $context; + } + + return null; + } + + /** + * Gets whether the reference is to be read from the root namespace. + */ + public function isFullyQualified(): bool + { + $context = $this->namespaceOrFullyQualified; + + if ($context instanceof Name) { + return $context->isFullyQualified; + } + + return $context; + } + + /** + * A helper method to set whether the reference is to be read from + * the root namespace. + */ + public function setFullyQualified(bool $isFullyQualified = true): void + { + $context = $this->namespaceOrFullyQualified; + + if ($context instanceof Name) { + $this->namespaceOrFullyQualified = $isFullyQualified + ? $context->toFullQualified() + : $context->toUnqualified(); + + return; + } + + $this->namespaceOrFullyQualified = $isFullyQualified; + } + + public function __get(string $property): Name|bool|null + { + return match ($property) { + 'namespace' => $this->getNamespace(), + 'isFullyQualified' => $this->isFullyQualified(), + default => throw new \Error(\sprintf('Undefined property %s::$%s', self::class, $property)), + }; + } - public function __toString(): string + public function __isset(string $name): bool { - return $this->name->toString() . '*'; + return \in_array($name, ['namespace', 'isFullyQualified'], true); } } diff --git a/libs/types/src/Identifier.php b/libs/types/src/Identifier.php index 579e4c1..212095e 100644 --- a/libs/types/src/Identifier.php +++ b/libs/types/src/Identifier.php @@ -5,6 +5,16 @@ namespace TypeLang\Type; /** + * A single word a name is made of. + * + * ``` + * Some\Any + * ^^^^ ^^^ // two identifiers + * + * non-empty-string + * ^^^^^^^^^^^^^^^^ // a single identifier + * ``` + * * @phpstan-consistent-constructor */ final class Identifier extends Node implements \Stringable @@ -12,7 +22,7 @@ final class Identifier extends Node implements \Stringable /** * @var list */ - protected const array SPECIAL_CLASS_NAME = [ + protected const SPECIAL_CLASS_NAME = [ 'self', 'parent', 'static', @@ -21,7 +31,7 @@ final class Identifier extends Node implements \Stringable /** * @var list */ - protected const array BUILTIN_TYPE_NAME = [ + protected const BUILTIN_TYPE_NAME = [ 'mixed', 'string', 'int', @@ -39,40 +49,27 @@ final class Identifier extends Node implements \Stringable ]; /** - * Returns {@see true} if the identifier contains the name of - * a "virtual" type, i.e. invalid in the PHP namespace. - * - * - `SomeClass` - Non-virtual, can be a type in PHP. - * - `false` - Non-virtual, can be a type in PHP. - * - `non-empty-array` - Virtual, cannot be defined in PHP. - * - `empty-string` - Virtual, cannot be defined in PHP. - */ - public bool $isVirtual { - get => \str_contains($this->value, '-'); - } - - /** - * Returns {@see true} in case of name contains special class reference. + * @param int<0, max> $offset + * @throws \InvalidArgumentException in case of an empty value */ - public bool $isSpecial { - get => self::isLooksLikeSpecial($this->value); - } - - /** - * Returns {@see true} in case of name contains builtin type name. - */ - public bool $isBuiltin { - get => self::isLooksLikeBuiltin($this->value); - } - public function __construct( /** * @var non-empty-string */ public readonly string $value, - ) {} + int $offset = 0, + ) { + if ($value === '') { + throw new \InvalidArgumentException('Name identifier cannot be empty'); + } + + parent::__construct($offset); + } - public static function createFromString(string|\Stringable $value): self + /** + * @param int<0, max> $offset + */ + public static function createFromString(string|\Stringable $value, int $offset = 0): self { if ($value instanceof self) { return $value; @@ -84,7 +81,7 @@ public static function createFromString(string|\Stringable $value): self throw new \InvalidArgumentException('Name identifier cannot be empty'); } - return new self($normalized); + return new self($normalized, $offset); } /** @@ -105,6 +102,36 @@ public static function isLooksLikeBuiltin(string $value): bool return \in_array(\strtolower($value), self::BUILTIN_TYPE_NAME, true); } + /** + * Returns {@see true} if the identifier contains the name of a "virtual" + * type, i.e. invalid in the PHP namespace. + * + * - `SomeClass` - Non-virtual, can be a type in PHP. + * - `false` - Non-virtual, can be a type in PHP. + * - `non-empty-array` - Virtual, cannot be defined in PHP. + * - `empty-string` - Virtual, cannot be defined in PHP. + */ + public function isVirtual(): bool + { + return \str_contains($this->value, '-'); + } + + /** + * Returns {@see true} in case of name contains special class reference. + */ + public function isSpecial(): bool + { + return self::isLooksLikeSpecial($this->value); + } + + /** + * Returns {@see true} in case of name contains builtin type name. + */ + public function isBuiltin(): bool + { + return self::isLooksLikeBuiltin($this->value); + } + /** * Returns name as string. * diff --git a/libs/types/src/IntersectionTypeNode.php b/libs/types/src/IntersectionTypeNode.php index d1f0255..58f7aab 100644 --- a/libs/types/src/IntersectionTypeNode.php +++ b/libs/types/src/IntersectionTypeNode.php @@ -5,6 +5,12 @@ namespace TypeLang\Type; /** + * All of several types at once. + * + * ``` + * Some\Any & Stringable + * ``` + * * @template T of TypeNode = TypeNode * * @template-extends LogicalTypeNode diff --git a/libs/types/src/Literal/BoolLiteralNode.php b/libs/types/src/Literal/BoolLiteralNode.php index 1cef959..4a6ee94 100644 --- a/libs/types/src/Literal/BoolLiteralNode.php +++ b/libs/types/src/Literal/BoolLiteralNode.php @@ -5,21 +5,26 @@ namespace TypeLang\Type\Literal; /** - * @template-extends LiteralNode + * A `true` or a `false`, in any case they are written in. + * + * ``` + * TruE // value: true, raw: "TruE" + * ``` + * + * @template-extends ScalarNode * * @phpstan-consistent-constructor */ -final class BoolLiteralNode extends LiteralNode implements ParsableLiteralNodeInterface +final class BoolLiteralNode extends ScalarNode { + /** + * @param int<0, max> $offset + */ public function __construct( bool $value, ?string $raw = null, + int $offset = 0, ) { - parent::__construct($value, $raw ?? ($value ? 'true' : 'false')); - } - - public static function parse(string $value): self - { - return new self(\strtolower($value) === 'true', $value); + parent::__construct($value, $raw ?? ($value ? 'true' : 'false'), $offset); } } diff --git a/libs/types/src/Literal/FloatLiteralNode.php b/libs/types/src/Literal/FloatLiteralNode.php index e6dceb2..45e6a3a 100644 --- a/libs/types/src/Literal/FloatLiteralNode.php +++ b/libs/types/src/Literal/FloatLiteralNode.php @@ -5,25 +5,27 @@ namespace TypeLang\Type\Literal; /** - * @template-extends LiteralNode + * A floating point number. + * + * ``` + * -1.5e+3 // value: -1500.0, raw: "-1.5e+3" + * .42 // value: 0.42, raw: ".42" + * ``` + * + * @template-extends ScalarNode * * @phpstan-consistent-constructor */ -final class FloatLiteralNode extends LiteralNode implements ParsableLiteralNodeInterface +final class FloatLiteralNode extends ScalarNode { + /** + * @param int<0, max> $offset + */ public function __construct( float $value, ?string $raw = null, + int $offset = 0, ) { - parent::__construct($value, $raw ?? (string) $this->value); - } - - public static function parse(string $value): self - { - if (!\is_numeric($value)) { - return new self(0.0, $value); - } - - return new self((float) $value, $value); + parent::__construct($value, $raw ?? (string) $value, $offset); } } diff --git a/libs/types/src/Literal/IntLiteralNode.php b/libs/types/src/Literal/IntLiteralNode.php index d6b3003..c8e9aaf 100644 --- a/libs/types/src/Literal/IntLiteralNode.php +++ b/libs/types/src/Literal/IntLiteralNode.php @@ -5,75 +5,39 @@ namespace TypeLang\Type\Literal; /** - * @template-extends LiteralNode + * A whole number, written in any of the four radixes. + * + * ``` + * 0xFE_DE // value: 65246, raw: "0xFE_DE", decimal: "65246" + * 042 // value: 34, raw: "042", decimal: "34" + * ``` + * + * @template-extends ScalarNode * * @phpstan-consistent-constructor */ -final class IntLiteralNode extends LiteralNode implements ParsableLiteralNodeInterface +final class IntLiteralNode extends ScalarNode { /** + * The value written out in base 10, so that a number too large for the + * platform's `int` is still readable in full. + * * @var numeric-string */ public readonly string $decimal; /** * @param numeric-string|null $decimal + * @param int<0, max> $offset */ public function __construct( int $value, ?string $raw = null, ?string $decimal = null, + int $offset = 0, ) { - $this->decimal = $decimal ?? (string) $this->value; - - parent::__construct($value, $raw ?? (string) $this->value); - } - - public static function parse(string $value): self - { - [$negative, $decimal] = self::split($value); - - $inverse = '-' . $decimal; - - if ($negative) { - if ((string) \PHP_INT_MIN === $inverse) { - return new self(\PHP_INT_MIN, $value, $inverse); - } - - /** @phpstan-ignore-next-line : An "$inverse" variable contain numeric-string */ - return new self((int) $inverse, $value, $inverse); - } - - return new self((int) $decimal, $value, $decimal); - } - - /** - * @return array{bool, numeric-string} - */ - private static function split(string $literal): array - { - $literal = \str_replace('_', '', $literal); - - if ($negative = ($literal[0] === '-')) { - $literal = \substr($literal, 1); - } - - // One of: [ 0123, 0o23, 0x00, 0b01 ] - if ($literal[0] === '0' && isset($literal[1])) { - /** @var array{bool, numeric-string} */ - return [$negative, match ($literal[1]) { - // hexadecimal - 'x', 'X' => \base_convert(\substr($literal, 2), 16, 10), - // binary - 'b', 'B' => \base_convert(\substr($literal, 2), 2, 10), - // octal - 'o', 'O' => \base_convert(\substr($literal, 2), 8, 10), - // octal (legacy) - default => \base_convert($literal, 8, 10), - }]; - } + $this->decimal = $decimal ?? (string) $value; - /** @var array{bool, numeric-string} */ - return [$negative, $literal]; + parent::__construct($value, $raw ?? (string) $value, $offset); } } diff --git a/libs/types/src/Literal/LiteralNode.php b/libs/types/src/Literal/LiteralNode.php index 6b490bb..8238d6e 100644 --- a/libs/types/src/Literal/LiteralNode.php +++ b/libs/types/src/Literal/LiteralNode.php @@ -7,19 +7,39 @@ use TypeLang\Type\TypeNode; /** + * A value standing where a type is expected, kept both as PHP sees it + * and as it is written. + * + * The two are not the same thing. A value is what PHP would compute, while + * a raw is the very text it was computed from, quotes, radix and all. + * + * ``` + * 0x1F // value: 31, raw: "0x1F" + * '\x41' // value: "\x41", raw: "'\x41'" + * ``` + * * @template TValue of mixed = mixed * * @template-implements LiteralNodeInterface + * + * @property-read TValue $value Gets a PHP representation of the literal value. + * @property-read string $raw Gets the original literal value specified in the token. */ abstract class LiteralNode extends TypeNode implements LiteralNodeInterface { + /** + * @param int<0, max> $offset + */ public function __construct( /** * @var TValue */ public readonly mixed $value, public readonly string $raw, - ) {} + int $offset = 0, + ) { + parent::__construct($offset); + } public function __toString(): string { diff --git a/libs/types/src/Literal/LiteralNodeInterface.php b/libs/types/src/Literal/LiteralNodeInterface.php index cababb6..3300cae 100644 --- a/libs/types/src/Literal/LiteralNodeInterface.php +++ b/libs/types/src/Literal/LiteralNodeInterface.php @@ -5,26 +5,15 @@ namespace TypeLang\Type\Literal; /** + * A value standing where a type is expected. + * * @template-covariant TValue of mixed = mixed + * + * @property-read TValue $value Gets a PHP representation of the literal value. + * @property-read string $raw Gets the original literal value specified in the token. */ interface LiteralNodeInterface extends \Stringable { - /** - * Gets a PHP representation of the literal value. - * - * @var TValue - */ - public mixed $value { - get; - } - - /** - * Gets the original literal value specified in the token. - */ - public string $raw { - get; - } - /** * Returns the processed ({@see $value}) literal value as a string. */ diff --git a/libs/types/src/Literal/NullLiteralNode.php b/libs/types/src/Literal/NullLiteralNode.php index 98c695e..2fe8480 100644 --- a/libs/types/src/Literal/NullLiteralNode.php +++ b/libs/types/src/Literal/NullLiteralNode.php @@ -5,12 +5,21 @@ namespace TypeLang\Type\Literal; /** + * A `null`, in any case it is written in. + * + * ``` + * NulL // value: null, raw: "NulL" + * ``` + * * @template-extends LiteralNode */ final class NullLiteralNode extends LiteralNode { - public function __construct(?string $raw = null) + /** + * @param int<0, max> $offset + */ + public function __construct(?string $raw = null, int $offset = 0) { - parent::__construct(null, $raw ?? 'null'); + parent::__construct(null, $raw ?? 'null', $offset); } } diff --git a/libs/types/src/Literal/ParsableLiteralNodeInterface.php b/libs/types/src/Literal/ParsableLiteralNodeInterface.php deleted file mode 100644 index aac49ec..0000000 --- a/libs/types/src/Literal/ParsableLiteralNodeInterface.php +++ /dev/null @@ -1,15 +0,0 @@ - + */ +abstract class ScalarNode extends LiteralNode {} diff --git a/libs/types/src/Literal/StringLiteralNode.php b/libs/types/src/Literal/StringLiteralNode.php index 553bd24..6628965 100644 --- a/libs/types/src/Literal/StringLiteralNode.php +++ b/libs/types/src/Literal/StringLiteralNode.php @@ -5,183 +5,28 @@ namespace TypeLang\Type\Literal; /** - * @template-extends LiteralNode + * A string, kept both decoded and as it is written. + * + * ``` + * "\x41\x42" // value: "AB", raw: "\"\x41\x42\"" + * ``` + * + * @template-extends ScalarNode * * @phpstan-consistent-constructor */ -final class StringLiteralNode extends LiteralNode implements ParsableLiteralNodeInterface +final class StringLiteralNode extends ScalarNode { /** - * @var non-empty-string + * @param int<0, max> $offset */ - private const string UTF_SEQUENCE_PATTERN = '/(? - */ - private const array ESCAPED_CHARS = [ - '\n' => "\n", - '\r' => "\r", - '\t' => "\t", - '\v' => "\v", - '\e' => "\e", - '\f' => "\f", - '\$' => '$', - ]; - final public function __construct( string $value, ?string $raw = null, + int $offset = 0, ) { - $raw ??= \sprintf('"%s"', \addcslashes($value, '"')); - - parent::__construct($value, $raw); - } - - public static function parse(string $value): self - { - if (\strlen($value) < 2) { - throw new \InvalidArgumentException('Could not parse non-quoted string'); - } - - if ($value[0] === '"') { - return self::createFromDoubleQuotedString($value); - } - - return self::createFromSingleQuotedString($value); - } - - /** - * @param non-empty-string $value - */ - public static function createFromDoubleQuotedString(string $value): self - { - if (\strlen($value) < 2) { - throw new \InvalidArgumentException('Could not parse non-quoted string'); - } - - $body = \substr($value, 1, -1); - - return self::parseEncodedValue( - string: \str_replace('\"', '"', $body), - raw: $value, - ); - } - - /** - * @param non-empty-string $value - */ - public static function createFromSingleQuotedString(string $value): self - { - if (\strlen($value) < 2) { - throw new \InvalidArgumentException('Could not parse non-quoted string'); - } - - $body = \substr($value, 1, -1); - - return new self( - value: \str_replace("\'", "'", $body), - raw: $value, - ); - } - - private static function parseEncodedValue(string $string, ?string $raw = null): self - { - $raw ??= $string; - - if (\str_contains($string, '\\')) { - // Replace double backslash to "\0" - $string = \str_replace('\\\\', "\0", $string); - - // Replace escaped chars (like a "\n") to real bytes - $string = self::renderEscapeSequences($string); - - // Replace hex sequences (like a "\xFF") to real bytes - $string = self::renderHexadecimalSequences($string); - - // Replace unicode sequences (like a "\u{FFFF}") to real bytes - $string = self::renderUtfSequences($string); - - // Rollback double backslash escaping - $string = \str_replace("\0", '\\\\', $string); - } - - return new self($string, $raw); - } - - /** - * Method for parsing and decode special escaped character sequences - * like a "\n", "\r" etc... - * - * @link https://www.php.net/manual/en/language.types.string.php - */ - private static function renderEscapeSequences(string $body): string - { - return \str_replace( - \array_keys(self::ESCAPED_CHARS), - \array_values(self::ESCAPED_CHARS), - $body, - ); - } - - /** - * Method for parsing and decode hexadecimal character sequences - * like a "\xFF" type. - * - * @link https://www.php.net/manual/en/language.types.string.php - */ - private static function renderHexadecimalSequences(string $body): string - { - $callback = static fn(array $matches): string - // @phpstan-ignore-next-line : A hexdec returns int<0, 255> - => \chr((int) \hexdec($matches[1])); - - return @\preg_replace_callback(self::HEX_SEQUENCE_PATTERN, $callback, $body) - ?? $body; - } - - /** - * Method for parsing and decode utf-8 character sequences - * like a "\u{FFFF}" type. - * - * @link https://www.php.net/manual/en/language.types.string.php - */ - private static function renderUtfSequences(string $body): string - { - return @\preg_replace_callback(self::UTF_SEQUENCE_PATTERN, static function (array $matches): string { - /** @var int<0, 1114112> $code */ - $code = (int) \hexdec($matches[1]); - - // @phpstan-ignore-next-line : PHPStan false-positive mb_chr evaluation - if (\function_exists('\\mb_chr') && ($result = \mb_chr($code)) !== false) { - return $result; - } - - if (0x80 > $code %= 0x200000) { - // @phpstan-ignore-next-line : Code is valid - return \chr($code); - } - - if (0x800 > $code) { - return \chr(0xC0 | $code >> 6) - . \chr(0x80 | $code & 0x3F); - } - - if (0x10000 > $code) { - return \chr(0xE0 | $code >> 12) - . \chr(0x80 | $code >> 6 & 0x3F) - . \chr(0x80 | $code & 0x3F); - } + $raw ??= \sprintf("'%s'", \addcslashes($value, "'")); - return \chr(0xF0 | $code >> 18) - . \chr(0x80 | $code >> 12 & 0x3F) - . \chr(0x80 | $code >> 6 & 0x3F) - . \chr(0x80 | $code & 0x3F); - }, $body) ?? $body; + parent::__construct($value, $raw, $offset); } } diff --git a/libs/types/src/Literal/VariableLiteralNode.php b/libs/types/src/Literal/VariableLiteralNode.php deleted file mode 100644 index a80ed97..0000000 --- a/libs/types/src/Literal/VariableLiteralNode.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * @phpstan-consistent-constructor - */ -final class VariableLiteralNode extends LiteralNode implements ParsableLiteralNodeInterface -{ - /** - * @param non-empty-string $value - */ - public function __construct(string $value) - { - if (\strlen($value) < 2) { - throw new \InvalidArgumentException('Variable name length must be greater than 1'); - } - - if (!\str_starts_with($value, '$')) { - throw new \InvalidArgumentException('Variable name must start with "$" character'); - } - - /** @var non-empty-string $normalized */ - $normalized = \substr($value, 1); - - parent::__construct($normalized, $value); - } - - public static function parse(string $value): self - { - if (!\str_starts_with($value, '$')) { - $value = '$' . $value; - } - - return new self($value); - } -} diff --git a/libs/types/src/LogicalTypeNode.php b/libs/types/src/LogicalTypeNode.php index 55ee9b6..e58a895 100644 --- a/libs/types/src/LogicalTypeNode.php +++ b/libs/types/src/LogicalTypeNode.php @@ -5,77 +5,159 @@ namespace TypeLang\Type; /** + * Several types joined by an operator, like a union or an intersection. + * + * A statement of the same kind is flattened into its parent, so an + * `A|(B|C)` holds the three of them side by side rather than a nested + * statement. + * * @template T of TypeNode = TypeNode * * @template-implements \IteratorAggregate + * @template-implements \ArrayAccess, T> */ -abstract class LogicalTypeNode extends TypeNode implements \IteratorAggregate, \Countable +abstract class LogicalTypeNode extends TypeNode implements + \IteratorAggregate, + \ArrayAccess, + \Countable { /** - * @var non-empty-list + * @var list{T, T, ...} */ public array $statements; + /** + * A logical statement accepts an arbitrary number of types, so there is no + * place left for an offset argument: use the {@see $offset} property. + * + * @param iterable $statements + * @param int<0, max> $offset + * @throws \LogicException in case of less than two statements are passed + */ public function __construct( - TypeNode $a, - TypeNode $b, - TypeNode ...$other, + iterable $statements, + int $offset = 0, ) { - // @phpstan-ignore-next-line : List of types cannot be empty - $this->statements = [...$this->unwrap([$a, $b, ...$other])]; + $statements = self::unwrap($statements); + + if (\count($statements) < 2) { + throw new \InvalidArgumentException('A logical statement must contain at least 2 elements'); + } + + // @phpstan-ignore-next-line : List of types contains at least 2 elements + $this->statements = $statements; + + parent::__construct($offset); } /** - * @param non-empty-list $statements - * @return iterable + * Flattens the statements of the same kind into a single list, so that + * a logical statement never contains a statement of its own kind. + * + * @param iterable $statements + * @return list */ - private function unwrap(array $statements): iterable + private static function unwrap(iterable $statements): array { + $result = []; + foreach ($statements as $statement) { if ($statement instanceof static) { - yield from $this->unwrap($statement->statements); - } else { - yield $statement; + foreach (self::unwrap($statement->statements) as $child) { + $result[] = $child; + } + + continue; } + + $result[] = $statement; } + + return $result; } - public function getIterator(): \Traversable + public function offsetExists(mixed $offset): bool { - return new \ArrayIterator($this->statements); + if (!\is_int($offset) || $offset < 0) { + throw new \InvalidArgumentException( + \sprintf('A %s::offsetExists($offset) must be an int<0, max>', static::class), + ); + } + + return isset($this->statements[$offset]); } - /** - * @return int<2, max> a logical statement must contain at least 2 elements - */ - public function count(): int + public function offsetGet(mixed $offset): ?Node { - /** @var int<2, max> */ - return \count($this->statements); + if (!\is_int($offset) || $offset < 0) { + throw new \InvalidArgumentException( + \sprintf('A %s::offsetGet($offset) must be an int<0, max>', static::class), + ); + } + + return $this->statements[$offset] ?? null; } - /** - * @return array{int<0, max>, non-empty-list} - */ - public function __serialize(): array + public function offsetSet(mixed $offset, mixed $value): void { - return [$this->offset, $this->statements]; + if (!$value instanceof Node) { + throw new \InvalidArgumentException( + \sprintf('A %s::offsetSet(..., $value) must be instance of %s', static::class, Node::class), + ); + } + + if ($offset === null) { + $this->statements[] = $value; + + return; + } + + if (!\is_int($offset) || $offset < 0) { + throw new \InvalidArgumentException( + \sprintf('A %s::offsetSet($offset, ...) must be an int<0, max>|null', static::class), + ); + } + + // @phpstan-ignore-next-line + $this->statements[$offset] = $value; + + if (!\array_is_list($this->statements)) { + $this->statements = \array_values($this->statements); + } + } + + public function offsetUnset(mixed $offset): void + { + if (!\is_int($offset) || $offset < 0) { + throw new \InvalidArgumentException( + \sprintf('A %s::offsetUnset($offset) must be an int<0, max>', static::class), + ); + } + + // @phpstan-ignore-next-line : Temporary allow stmt removing + unset($this->statements[$offset]); + + if (\count($this->statements) < 2) { + throw new \InvalidArgumentException('A logical statement must contain at least 2 elements'); + } + + if (!\array_is_list($this->statements)) { + // @phpstan-ignore-next-line : An array size already has been checked above + $this->statements = \array_values($this->statements); + } + } + + public function getIterator(): \Traversable + { + return new \ArrayIterator($this->statements); } /** - * @param array{0?: int<0, max>, 1?: non-empty-list} $data - * @throws \UnexpectedValueException + * @return int<2, max> a logical statement must contain at least 2 elements */ - public function __unserialize(array $data): void + public function count(): int { - $this->offset = $data[0] ?? throw new \UnexpectedValueException(\sprintf( - 'Unable to unserialize %s offset', - static::class, - )); - - $this->statements = $data[1] ?? throw new \UnexpectedValueException(\sprintf( - 'Unable to unserialize %s statements', - static::class, - )); + /** @var int<2, max> */ + return \count($this->statements); } } diff --git a/libs/types/src/MaskNode.php b/libs/types/src/MaskNode.php new file mode 100644 index 0000000..e85a6a6 --- /dev/null +++ b/libs/types/src/MaskNode.php @@ -0,0 +1,120 @@ + + */ +final class MaskNode extends NodeList implements \Stringable +{ + /** + * @param iterable $items + * @param int<0, max> $offset + */ + public function __construct(iterable $items, int $offset = 0) + { + parent::__construct($items, $offset); + + if (\count($this->items) < 1) { + throw new \InvalidArgumentException('A mask must have at least one item'); + } + } + + #[\Override] + public function offsetUnset(mixed $offset): void + { + parent::offsetUnset($offset); + + if (\count($this->items) < 1) { + throw new \UnderflowException('A mask must have at least one item'); + } + } + + /** + * Gets the literal segments of a mask, in the order they are written in, + * leaving every wildcard out. + * + * ``` + * // Some\Any::BAR*BAZ*SOME + * $mask->getSegments(); // [BAR, BAZ, SOME] + * + * // Some\Any::* + * $mask->getSegments(); // [] + * ``` + * + * @return list + */ + public function getSegments(): array + { + $result = []; + + foreach ($this->items as $item) { + if ($item instanceof Identifier) { + $result[] = $item; + } + } + + return $result; + } + + /** + * @return list + */ + public function getSegmentsAsStrings(): array + { + $result = []; + + foreach ($this->items as $item) { + if ($item instanceof Identifier) { + $result[] = $item->value; + } + } + + return $result; + } + + /** + * Returns a mask as the string it is written as. + * + * @return non-empty-string + */ + public function toString(): string + { + $result = ''; + + foreach ($this->items as $item) { + $result .= $item->toString(); + } + + /** @var non-empty-string */ + return $result; + } + + /** + * @return non-empty-string + */ + public function __toString(): string + { + return $this->toString(); + } +} diff --git a/libs/types/src/Name.php b/libs/types/src/Name.php index c9b6237..ba7af87 100644 --- a/libs/types/src/Name.php +++ b/libs/types/src/Name.php @@ -5,6 +5,17 @@ namespace TypeLang\Type; /** + * A name, that is, the identifiers ({@see Identifier}) it is written of and the + * namespace separators between them. + * + * ``` + * Some\Any + * ^^^^ ^^^ a relative name of two parts + * + * \Some\Any + * ^ a fully qualified name begins in a separator + * ``` + * * @phpstan-consistent-constructor * * @template-implements \IteratorAggregate @@ -14,102 +25,181 @@ final class Name extends Node implements \IteratorAggregate, \Countable, \String /** * @var non-empty-string */ - private const string NAMESPACE_DELIMITER = '\\'; + private const NAMESPACE_DELIMITER = '\\'; - public const bool IS_FULLY_QUALIFIED_DEFAULT_VALUE = false; + public const IS_FULLY_QUALIFIED_DEFAULT_VALUE = false; /** + * The identifiers a name is written of. + * * @var non-empty-list */ - public array $segments; + public readonly array $parts; /** - * Gets the first segment of a name + * The first of the {@see $parts}, which is the one an alias is resolved by. */ - public Identifier $first { - get => $this->segments[0]; - } + public readonly Identifier $first; /** - * Gets the last segment of a name + * The last of the {@see $parts}, which is the short name of a class. */ - public Identifier $last { - get => $this->segments[\count($this->segments) - 1]; - } + public readonly Identifier $last; /** - * Gets whether the name is simple. + * @param iterable $parts + * @param int<0, max> $offset + * @throws \InvalidArgumentException in case of parts are empty or contain + * something else than an {@see Identifier} */ - public bool $isSimple { - get => \count($this->segments) === 1; + public function __construct( + iterable $parts, + /** + * Whether a name is written with the leading separator that says it + * is to be read from the root and not from wherever it stands. + * + * ``` + * Some\Any // false + * \Some\Any // true + * ``` + */ + public readonly bool $isFullyQualified = self::IS_FULLY_QUALIFIED_DEFAULT_VALUE, + int $offset = 0, + ) { + $this->parts = self::formatNameParts($parts); + + $this->first = $this->parts[0]; + $this->last = $this->parts[\count($this->parts) - 1]; + + parent::__construct($offset); } /** - * Gets {@see true} in case of name contains special class reference. + * @param iterable $parts + * @return non-empty-list */ - public bool $isSpecial { - get => $this->isSimple && $this->first->isSpecial; + private static function formatNameParts(iterable $parts): array + { + $parts = match (true) { + $parts instanceof \Traversable => \iterator_to_array($parts, false), + \array_is_list($parts) => $parts, + default => \array_values($parts), + }; + + if ($parts === []) { + throw new \InvalidArgumentException('Name parts count can not be empty'); + } + + return $parts; } /** - * Gets {@see true} in case of name contains builtin type name. + * @param int<0, max> $offset */ - public bool $isBuiltin { - get => $this->isSimple && $this->first->isBuiltin; + public static function createFromString(string|\Stringable $name, int $offset = 0): self + { + $name = (string) $name; + $parts = []; + + foreach (\explode(self::NAMESPACE_DELIMITER, $name) as $segment) { + if ($segment === '') { + continue; + } + + $parts[] = Identifier::createFromString($segment); + } + + return new self($parts, \str_starts_with($name, self::NAMESPACE_DELIMITER), $offset); } /** - * @param iterable $segments + * Gets the first segment of a name */ - public function __construct( - iterable $segments, - public readonly bool $isFullyQualified = self::IS_FULLY_QUALIFIED_DEFAULT_VALUE, - ) { - $segments = \iterator_to_array($segments, false); - - \assert($segments !== [], new \InvalidArgumentException('Name segments count can not be empty')); + public function getFirstPart(): Identifier + { + return $this->first; + } - $this->segments = $segments; + /** + * Gets the first segment of a name as a string + * + * @return non-empty-string + */ + public function getFirstPartAsString(): string + { + return $this->first->toString(); } /** - * @param iterable $segments + * Gets the first segment of a name as a lowercase string + * + * @return non-empty-lowercase-string */ - public static function createFromStringSegments( - iterable $segments, - bool $isFullyQualified = self::IS_FULLY_QUALIFIED_DEFAULT_VALUE, - ): self { - $identifiers = []; + public function getFirstPartAsLowerString(): string + { + return $this->first->toLowerString(); + } - foreach ($segments as $segment) { - $identifiers[] = Identifier::createFromString($segment); - } + /** + * Gets the last segment of a name + */ + public function getLastPart(): Identifier + { + return $this->last; + } - return new self($identifiers, $isFullyQualified); + /** + * Gets the last segment of a name as a string + * + * @return non-empty-string + */ + public function getLastPartAsString(): string + { + return $this->last->toString(); } /** - * @param non-empty-string|\Stringable $name + * Gets the last segment of a name as a lowercase string + * + * @return non-empty-lowercase-string */ - public static function createFromString(string|\Stringable $name): self + public function getLastPartAsLowerString(): string { - $name = (string) $name; - $unqualified = \trim($name, self::NAMESPACE_DELIMITER); + return $this->last->toLowerString(); + } - $segments = []; + /** + * Gets whether the name is simple. + */ + public function isSimple(): bool + { + return \count($this->parts) === 1; + } - foreach (\explode(self::NAMESPACE_DELIMITER, $unqualified) as $segment) { - if ($segment === '') { - continue; - } + /** + * Gets {@see true} in case of name contains special class reference. + */ + public function isSpecial(): bool + { + return $this->isSimple() && $this->getFirstPart()->isSpecial(); + } - $segments[] = $segment; - } + /** + * Gets {@see true} in case of name contains builtin type name. + */ + public function isBuiltin(): bool + { + return $this->isSimple() && $this->getFirstPart()->isBuiltin(); + } - return self::createFromStringSegments( - segments: $segments, - isFullyQualified: \str_starts_with($name, self::NAMESPACE_DELIMITER), - ); + /** + * Gets {@see true} in case of name is fully qualified. + * + * @deprecated use the {@see $isFullyQualified} property instead + */ + public function isFullQualified(): bool + { + return $this->isFullyQualified; } /** @@ -119,7 +209,7 @@ public static function createFromString(string|\Stringable $name): self public function slice(int $offset = 0, ?int $length = null): self { return new self( - segments: \array_slice($this->segments, $offset, $length), + parts: \array_slice($this->parts, $offset, $length), isFullyQualified: $this->isFullyQualified, ); } @@ -140,8 +230,8 @@ public function slice(int $offset = 0, ?int $length = null): self public function withAdded(self $name): self { return new self([ - ...$this->segments, - ...$name->segments, + ...$this->parts, + ...$name->parts, ], $this->isFullyQualified); } @@ -183,8 +273,8 @@ public function withAdded(self $name): self public function mergeWith(self $name): self { return new self([ - ...$this->segments, - ...\array_slice($name->segments, 1), + ...$this->parts, + ...\array_slice($name->parts, 1), ], $this->isFullyQualified); } @@ -197,7 +287,7 @@ public function toFullQualified(): self return clone $this; } - return new self($this->segments, true); + return new self($this->parts, true); } /** @@ -206,20 +296,38 @@ public function toFullQualified(): self public function toUnqualified(): self { if ($this->isFullyQualified) { - return new self($this->segments, false); + return new self($this->parts, false); } return clone $this; } + /** + * @return non-empty-list + */ + public function toArray(): array + { + return $this->parts; + } + + /** + * @deprecated use the {@see $parts} property instead + * + * @return non-empty-list + */ + public function getParts(): array + { + return $this->parts; + } + /** * @return non-empty-list */ - public function toStringArray(): array + public function toArrayStrings(): array { $result = []; - foreach ($this->segments as $identifier) { + foreach ($this->parts as $identifier) { $result[] = $identifier->toString(); } @@ -227,13 +335,23 @@ public function toStringArray(): array } /** + * @deprecated use the {@see toArrayStrings()} method instead + * * @return non-empty-list */ - public function toLowercaseStringArray(): array + public function getPartsAsString(): array + { + return $this->toArrayStrings(); + } + + /** + * @return non-empty-list + */ + public function toArrayLowercaseStrings(): array { $result = []; - foreach ($this->segments as $identifier) { + foreach ($this->parts as $identifier) { $result[] = $identifier->toLowerString(); } @@ -261,7 +379,7 @@ public function toString(): string */ public function toUnqualifiedString(): string { - return \implode(self::NAMESPACE_DELIMITER, $this->toStringArray()); + return \implode(self::NAMESPACE_DELIMITER, $this->toArrayStrings()); } /** @@ -272,7 +390,7 @@ public function toUnqualifiedString(): string public function toFullQualifiedString(): string { return self::NAMESPACE_DELIMITER - . \implode(self::NAMESPACE_DELIMITER, $this->toStringArray()); + . \implode(self::NAMESPACE_DELIMITER, $this->toArrayStrings()); } /** @@ -302,7 +420,7 @@ public function toUnqualifiedLowerString(): string */ public function toFullQualifiedLowerString(): string { - return \strtolower($this->toUnqualifiedString()); + return \strtolower($this->toFullQualifiedString()); } /** @@ -310,7 +428,7 @@ public function toFullQualifiedLowerString(): string */ public function getIterator(): \Traversable { - return new \ArrayIterator($this->segments); + return new \ArrayIterator($this->parts); } /** @@ -318,7 +436,7 @@ public function getIterator(): \Traversable */ public function count(): int { - return \count($this->segments); + return \count($this->parts); } /** @@ -330,25 +448,27 @@ public function __toString(): string } /** - * @return array{int<0, max>, non-empty-list} + * @return array{non-empty-list, int<0, max>, bool} */ public function __serialize(): array { - return [$this->offset, $this->segments]; + return [$this->parts, $this->offset, $this->isFullyQualified]; } /** - * @param array{0?: int<0, max>, 1?: non-empty-list} $data + * @param array{0?: non-empty-list, 1?: int<0, max>, 2?: bool} $data * @throws \UnexpectedValueException */ public function __unserialize(array $data): void { - $this->offset = $data[0] ?? throw new \UnexpectedValueException( - message: 'Unable to unserialize Name offset', + $this->parts = $parts = $data[0] ?? throw new \UnexpectedValueException( + message: 'Unable to unserialize Name segments', ); - $this->segments = $data[1] ?? throw new \UnexpectedValueException( - message: 'Unable to unserialize Name identifier parts', - ); + $this->first = \reset($parts); + $this->last = \end($parts); + + $this->offset = $data[1] ?? 0; + $this->isFullyQualified = $data[2] ?? self::IS_FULLY_QUALIFIED_DEFAULT_VALUE; } } diff --git a/libs/types/src/NamedTypeNode.php b/libs/types/src/NamedTypeNode.php index 9a7d35a..8a07015 100644 --- a/libs/types/src/NamedTypeNode.php +++ b/libs/types/src/NamedTypeNode.php @@ -7,11 +7,57 @@ use TypeLang\Type\Shape\FieldsListNode; use TypeLang\Type\Template\TemplateArgumentListNode; +/** + * A type referenced by name, along with whatever the name is followed by. + * + * ``` + * Some\Any + * ^^^^^^^^ the name + * ^^^^^^^^^^^^^ the template arguments + * + * array{name: string} + * ^^^^^ the name + * ^^^^^^^^^^^^^^ the shape fields + * + * object{name: string, ...} + * ^^^^^^ the name + * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the shape fields + * ^^^^^^^^^^^^^^^ the template arguments + * ``` + */ final class NamedTypeNode extends TypeNode { + /** + * @param int<0, max> $offset + */ public function __construct( public Name $name, + /** + * Template arguments of a type, or {@see null} in case of the name is + * followed by none. + * + * The list is never empty, since an `Some<>` is no type. + * + * ``` + * Some // null + * Some // a list of 1 argument + * ``` + */ public ?TemplateArgumentListNode $arguments = null, + /** + * Shape fields of a type, or {@see null} in case of the name is + * followed by none. An empty list is not the same as a {@see null}: + * A shape may well be written with no fields at all. + * + * ``` + * array // null: no shape + * array{} // an empty list: a shape of no fields + * array{a: int} // a list of 1 field + * ``` + */ public ?FieldsListNode $fields = null, - ) {} + int $offset = 0, + ) { + parent::__construct($offset); + } } diff --git a/libs/types/src/Node.php b/libs/types/src/Node.php index 09c28db..51e3fe3 100644 --- a/libs/types/src/Node.php +++ b/libs/types/src/Node.php @@ -6,8 +6,17 @@ abstract class Node implements NodeInterface { - /** - * @var int<0, max> - */ - public int $offset = 0; + public function __construct( + /** + * Token offset defined in the source code. + * + * @var int<0, max> + */ + public int $offset = 0, + ) {} + + public function getOffset(): int + { + return $this->offset; + } } diff --git a/libs/types/src/NodeInterface.php b/libs/types/src/NodeInterface.php index 2880840..84b8b49 100644 --- a/libs/types/src/NodeInterface.php +++ b/libs/types/src/NodeInterface.php @@ -9,22 +9,7 @@ interface NodeInterface /** * Gets token offset defined in the source code. * - * It is recommended to use the `phplrt/position` package to determine - * the line and column from this information: - * - * ```php - * $position = Phplrt\Position\Position::fromOffset( - * source: \file_get_contents($filename), - * offset: $node->offset, - * ); - * - * echo 'line: ' . $position->getLine() . "\n" - * 'column: ' . $position->getColumn(); - * ``` - * - * @var int<0, max> + * @return int<0, max> */ - public int $offset { - get; - } + public function getOffset(): int; } diff --git a/libs/types/src/NodeList.php b/libs/types/src/NodeList.php index fcbee93..bf5e73d 100644 --- a/libs/types/src/NodeList.php +++ b/libs/types/src/NodeList.php @@ -5,7 +5,11 @@ namespace TypeLang\Type; /** + * An ordered list of nodes of one kind, like the arguments of a generic + * or the fields of a shape. + * * @template TNode of Node = Node + * @template TNonEmpty of bool = false * * @template-implements \IteratorAggregate * @template-implements \ArrayAccess, TNode> @@ -16,32 +20,49 @@ abstract class NodeList extends Node implements \Countable { /** - * @var list + * @var (TNonEmpty is true ? non-empty-list : list) */ public array $items = []; /** - * @var TNode|null + * @param iterable $items + * @param int<0, max> $offset */ - public ?Node $first { - get => $this->items[0] ?? null; + public function __construct(iterable $items = [], int $offset = 0) + { + $this->items = match (true) { + $items instanceof \Traversable => \iterator_to_array($items, false), + \array_is_list($items) => $items, + default => \array_values($items), + }; + + parent::__construct($offset); } /** - * @var TNode|null + * Gets the first node of a list or {@see null} in case of the list is empty. + * + * @return (TNonEmpty is true ? TNode : TNode|null) */ - public ?Node $last { - get => ($lastKey = \array_key_last($this->items)) !== null - ? $this->items[$lastKey] - : null; + public function first(): ?Node + { + return $this->items[0] ?? null; } /** - * @param iterable $items + * Gets the last node of a list or {@see null} in case of the list is empty. + * + * @return (TNonEmpty is true ? TNode : TNode|null) */ - public function __construct(iterable $items = []) + public function last(): ?Node { - $this->items = \iterator_to_array($items, false); + $key = \array_key_last($this->items); + + if ($key === null) { + return null; + } + + return $this->items[$key]; } /** @@ -67,26 +88,45 @@ public function findIndex(Node $node): ?int public function offsetExists(mixed $offset): bool { - // @phpstan-ignore-next-line - \assert(\is_int($offset) && $offset >= 0); + if (!\is_int($offset) || $offset < 0) { + throw new \InvalidArgumentException( + \sprintf('A %s::offsetExists($offset) must be an int<0, max>', static::class), + ); + } return isset($this->items[$offset]); } public function offsetGet(mixed $offset): ?Node { - // @phpstan-ignore-next-line - \assert(\is_int($offset) && $offset >= 0); + if (!\is_int($offset) || $offset < 0) { + throw new \InvalidArgumentException( + \sprintf('A %s::offsetGet($offset) must be an int<0, max>', static::class), + ); + } return $this->items[$offset] ?? null; } public function offsetSet(mixed $offset, mixed $value): void { - // @phpstan-ignore-next-line - \assert(\is_int($offset) && $offset >= 0); - // @phpstan-ignore-next-line - \assert($value instanceof Node); + if (!$value instanceof Node) { + throw new \InvalidArgumentException( + \sprintf('A %s::offsetSet(..., $value) must be instance of %s', static::class, Node::class), + ); + } + + if ($offset === null) { + $this->items[] = $value; + + return; + } + + if (!\is_int($offset) || $offset < 0) { + throw new \InvalidArgumentException( + \sprintf('A %s::offsetSet($offset, ...) must be an int<0, max>|null', static::class), + ); + } // @phpstan-ignore-next-line $this->items[$offset] = $value; @@ -98,12 +138,18 @@ public function offsetSet(mixed $offset, mixed $value): void public function offsetUnset(mixed $offset): void { + if (!\is_int($offset) || $offset < 0) { + throw new \InvalidArgumentException( + \sprintf('A %s::offsetUnset($offset) must be an int<0, max>', static::class), + ); + } + // @phpstan-ignore-next-line - \assert(\is_int($offset) && $offset >= 0); + unset($this->items[$offset]); - $items = $this->items; - unset($items[$offset]); - $this->items = \array_values($items); + if (!\array_is_list($this->items)) { + $this->items = \array_values($this->items); + } } public function getIterator(): \Traversable @@ -112,7 +158,7 @@ public function getIterator(): \Traversable } /** - * @return int<0, max> + * @return (TNonEmpty is true ? int<1, max> : int<0, max>) */ public function count(): int { diff --git a/libs/types/src/NullableTypeNode.php b/libs/types/src/NullableTypeNode.php index 5db00b3..25e2d8a 100644 --- a/libs/types/src/NullableTypeNode.php +++ b/libs/types/src/NullableTypeNode.php @@ -5,6 +5,12 @@ namespace TypeLang\Type; /** + * A type a `null` is allowed besides, written with a leading `?`. + * + * ``` + * ?Some\Any + * ``` + * * @template T of TypeNode = TypeNode * * @template-extends WrappingTypeNode diff --git a/libs/types/src/Shape/ClassConstFieldNode.php b/libs/types/src/Shape/ClassConstFieldNode.php deleted file mode 100644 index 2f17d40..0000000 --- a/libs/types/src/Shape/ClassConstFieldNode.php +++ /dev/null @@ -1,36 +0,0 @@ - - */ -final class ClassConstFieldNode extends ExplicitFieldNode -{ - public string $index { - get => \vsprintf('%s::%s', [ - $this->key->class->toString(), - $this->key->constant->toString(), - ]); - } - - public function __construct( - ClassConstNode $key, - TypeNode $type, - bool $isOptional = false, - ?AttributeGroupListNode $attributes = null, - ) { - parent::__construct( - key: $key, - type: $type, - isOptional: $isOptional, - attributes: $attributes, - ); - } -} diff --git a/libs/types/src/Shape/ClassConstMaskFieldNode.php b/libs/types/src/Shape/ClassConstMaskFieldNode.php deleted file mode 100644 index 6e4089a..0000000 --- a/libs/types/src/Shape/ClassConstMaskFieldNode.php +++ /dev/null @@ -1,36 +0,0 @@ - - */ -final class ClassConstMaskFieldNode extends ExplicitFieldNode -{ - public string $index { - get => \vsprintf('%s::%s*', [ - $this->key->class->toString(), - $this->key->constant?->toString(), - ]); - } - - public function __construct( - ClassConstMaskNode $key, - TypeNode $type, - bool $isOptional = false, - ?AttributeGroupListNode $attributes = null, - ) { - parent::__construct( - key: $key, - type: $type, - isOptional: $isOptional, - attributes: $attributes, - ); - } -} diff --git a/libs/types/src/Shape/ComplexFieldNode.php b/libs/types/src/Shape/ComplexFieldNode.php new file mode 100644 index 0000000..72229d7 --- /dev/null +++ b/libs/types/src/Shape/ComplexFieldNode.php @@ -0,0 +1,50 @@ + + */ +final class ComplexFieldNode extends ExplicitFieldNode +{ + /** + * @param int<0, max> $offset + */ + public function __construct( + TypeNode $key, + TypeNode $type, + bool $isOptional = false, + int $offset = 0, + ) { + parent::__construct( + key: $key, + type: $type, + isOptional: $isOptional, + offset: $offset, + ); + } +} diff --git a/libs/types/src/Shape/ConstMaskFieldNode.php b/libs/types/src/Shape/ConstMaskFieldNode.php deleted file mode 100644 index 6642a86..0000000 --- a/libs/types/src/Shape/ConstMaskFieldNode.php +++ /dev/null @@ -1,33 +0,0 @@ - - */ -final class ConstMaskFieldNode extends ExplicitFieldNode -{ - public string $index { - get => (string) $this->key; - } - - public function __construct( - ConstMaskNode $key, - TypeNode $type, - bool $isOptional = false, - ?AttributeGroupListNode $attributes = null, - ) { - parent::__construct( - key: $key, - type: $type, - isOptional: $isOptional, - attributes: $attributes, - ); - } -} diff --git a/libs/types/src/Shape/ExplicitFieldNode.php b/libs/types/src/Shape/ExplicitFieldNode.php index 6e90c96..db1f22c 100644 --- a/libs/types/src/Shape/ExplicitFieldNode.php +++ b/libs/types/src/Shape/ExplicitFieldNode.php @@ -4,34 +4,44 @@ namespace TypeLang\Type\Shape; -use TypeLang\Type\Attribute\AttributeGroupListNode; use TypeLang\Type\TypeNode; /** + * A field of a shape written with a key of its own. + * + * What the key may be is what tells the children of this class apart. + * A {@see NamedFieldNode} carries a bare word, a {@see ScalarFieldNode} + * a scalar value and a {@see ComplexFieldNode} a reference that has to + * be read. + * + * A key that comes down to a string of its own is marked by the + * {@see SimpleFieldNodeInterface}. That string is what a shape tells its + * keys apart by. + * * @template TKey of mixed */ abstract class ExplicitFieldNode extends FieldNode { /** - * Gets a pretty-printed string representation of the key + * @param int<0, max> $offset */ - abstract public string $index { - get; - } - public function __construct( /** + * The key a field is written with. Its kind is what the children of + * this class differ in, so a child narrows it down to a node of its + * own. + * * @var TKey */ public mixed $key, TypeNode $type, bool $isOptional = false, - ?AttributeGroupListNode $attributes = null, + int $offset = 0, ) { parent::__construct( type: $type, isOptional: $isOptional, - attributes: $attributes, + offset: $offset, ); } } diff --git a/libs/types/src/Shape/FieldNode.php b/libs/types/src/Shape/FieldNode.php index 6165712..12a09d4 100644 --- a/libs/types/src/Shape/FieldNode.php +++ b/libs/types/src/Shape/FieldNode.php @@ -4,20 +4,38 @@ namespace TypeLang\Type\Shape; -use TypeLang\Type\Attribute\AttributeGroupListNode; use TypeLang\Type\Node; use TypeLang\Type\TypeNode; -abstract class FieldNode extends Node implements \Stringable +/** + * A single field of a shape. + * + * A shape is written either way throughout, never both at once. + * + * ``` + * array{name: string, id: int} // fields written with a key + * array{string, int} // fields written with none + * ``` + */ +abstract class FieldNode extends Node { + /** + * @param int<0, max> $offset + */ public function __construct( public TypeNode $type, + /** + * Whether a field may be absent, written as a `?` in front of + * the colon. + * + * ``` + * array{name: string} // false + * array{name?: string} // true + * ``` + */ public bool $isOptional = false, - public ?AttributeGroupListNode $attributes = null, - ) {} - - public function __toString(): string - { - return $this->isOptional ? 'optional' : 'required'; + int $offset = 0, + ) { + parent::__construct($offset); } } diff --git a/libs/types/src/Shape/FieldsListNode.php b/libs/types/src/Shape/FieldsListNode.php index 71ada23..fc54e08 100644 --- a/libs/types/src/Shape/FieldsListNode.php +++ b/libs/types/src/Shape/FieldsListNode.php @@ -7,22 +7,37 @@ use TypeLang\Type\NodeList; /** + * The fields of a shape, in the order they are written in. + * + * An empty list is a shape written with no fields at all, which is a thing + * of its own. + * + * An `array` carries no list, while an `array{}` carries an empty one. + * See the {@see \TypeLang\Type\NamedTypeNode::$fields}. + * * @template-extends NodeList */ -final class FieldsListNode extends NodeList implements \Stringable +final class FieldsListNode extends NodeList { /** * @param list $list + * @param int<0, max> $offset */ public function __construct( array $list = [], - public bool $sealed = true, + /** + * Whether the shape is written with the fields it lists and no other. + * An unsealed one ends in a `...`, which says that whatever else it + * carries was left unsaid. + * + * ``` + * array{a: int} // sealed + * array{a: int, ...} // unsealed + * ``` + */ + public bool $isSealed = true, + int $offset = 0, ) { - parent::__construct($list); - } - - public function __toString(): string - { - return $this->sealed ? 'sealed' : 'unsealed'; + parent::__construct($list, $offset); } } diff --git a/libs/types/src/Shape/ImplicitFieldNode.php b/libs/types/src/Shape/ImplicitFieldNode.php index 043e7a6..385face 100644 --- a/libs/types/src/Shape/ImplicitFieldNode.php +++ b/libs/types/src/Shape/ImplicitFieldNode.php @@ -4,4 +4,13 @@ namespace TypeLang\Type\Shape; +/** + * A field of a shape written with no key at all, the way an element of + * a plain list is. + * + * ``` + * array{int, string} + * // ^^^ ^^^^^^ two implicit fields + * ``` + */ final class ImplicitFieldNode extends FieldNode {} diff --git a/libs/types/src/Shape/NamedFieldNode.php b/libs/types/src/Shape/NamedFieldNode.php index dfdb0db..caba21b 100644 --- a/libs/types/src/Shape/NamedFieldNode.php +++ b/libs/types/src/Shape/NamedFieldNode.php @@ -4,30 +4,43 @@ namespace TypeLang\Type\Shape; -use TypeLang\Type\Attribute\AttributeGroupListNode; use TypeLang\Type\Identifier; use TypeLang\Type\TypeNode; /** + * A field of a shape keyed by a bare word. + * + * ``` + * array{name: string} + * // ^^^^ the key + * ``` + * + * A `true`, a `false` and a `null` are bare words as well. In the place of + * a key they name nothing but themselves. + * * @template-extends ExplicitFieldNode */ -final class NamedFieldNode extends ExplicitFieldNode +final class NamedFieldNode extends ExplicitFieldNode implements SimpleFieldNodeInterface { - public string $index { - get => $this->key->toString(); - } - + /** + * @param int<0, max> $offset + */ public function __construct( Identifier $key, TypeNode $type, bool $isOptional = false, - ?AttributeGroupListNode $attributes = null, + int $offset = 0, ) { parent::__construct( key: $key, type: $type, isOptional: $isOptional, - attributes: $attributes, + offset: $offset, ); } + + public function getIndex(): string + { + return $this->key->toString(); + } } diff --git a/libs/types/src/Shape/NumericFieldNode.php b/libs/types/src/Shape/NumericFieldNode.php deleted file mode 100644 index fd14ed0..0000000 --- a/libs/types/src/Shape/NumericFieldNode.php +++ /dev/null @@ -1,33 +0,0 @@ - - */ -final class NumericFieldNode extends ExplicitFieldNode -{ - public string $index { - get => (string) $this->key->value; - } - - public function __construct( - IntLiteralNode $key, - TypeNode $type, - bool $isOptional = false, - ?AttributeGroupListNode $attributes = null, - ) { - parent::__construct( - key: $key, - type: $type, - isOptional: $isOptional, - attributes: $attributes, - ); - } -} diff --git a/libs/types/src/Shape/ScalarFieldNode.php b/libs/types/src/Shape/ScalarFieldNode.php new file mode 100644 index 0000000..645aa01 --- /dev/null +++ b/libs/types/src/Shape/ScalarFieldNode.php @@ -0,0 +1,55 @@ + + */ +final class ScalarFieldNode extends ExplicitFieldNode implements SimpleFieldNodeInterface +{ + /** + * @param int<0, max> $offset + */ + public function __construct( + IntLiteralNode|StringLiteralNode $key, + TypeNode $type, + bool $isOptional = false, + int $offset = 0, + ) { + parent::__construct( + key: $key, + type: $type, + isOptional: $isOptional, + offset: $offset, + ); + } + + public function getIndex(): string + { + /** @var non-empty-string */ + return (string) $this->key->value; + } +} diff --git a/libs/types/src/Shape/SimpleFieldNodeInterface.php b/libs/types/src/Shape/SimpleFieldNodeInterface.php new file mode 100644 index 0000000..3984ed9 --- /dev/null +++ b/libs/types/src/Shape/SimpleFieldNodeInterface.php @@ -0,0 +1,34 @@ + - */ -final class StringNamedFieldNode extends ExplicitFieldNode -{ - public string $index { - get => $this->key->value; - } - - public function __construct( - StringLiteralNode $key, - TypeNode $type, - bool $isOptional = false, - ?AttributeGroupListNode $attributes = null, - ) { - parent::__construct( - key: $key, - type: $type, - isOptional: $isOptional, - attributes: $attributes, - ); - } -} diff --git a/libs/types/src/Template/TemplateArgumentListNode.php b/libs/types/src/Template/TemplateArgumentListNode.php index 248757a..778896a 100644 --- a/libs/types/src/Template/TemplateArgumentListNode.php +++ b/libs/types/src/Template/TemplateArgumentListNode.php @@ -7,6 +7,41 @@ use TypeLang\Type\NodeList; /** - * @template-extends NodeList + * Template arguments a type is referenced with, in the order they are + * written in. + * + * Note that an empty template argument list CANNOT be created, and this is + * an undefined behavior. + * + * ``` + * Some\Any + * // ^^^ ^^^^^^ two arguments + * ``` + * + * @template-extends NodeList */ -final class TemplateArgumentListNode extends NodeList {} +final class TemplateArgumentListNode extends NodeList +{ + /** + * @param iterable $items + * @param int<0, max> $offset + */ + public function __construct(iterable $items, int $offset = 0) + { + parent::__construct($items, $offset); + + if (\count($this->items) < 1) { + throw new \InvalidArgumentException('Template arguments must have at least one item'); + } + } + + #[\Override] + public function offsetUnset(mixed $offset): void + { + parent::offsetUnset($offset); + + if (\count($this->items) < 1) { + throw new \UnderflowException('Template arguments must have at least one item'); + } + } +} diff --git a/libs/types/src/Template/TemplateArgumentNode.php b/libs/types/src/Template/TemplateArgumentNode.php index e7dbb62..18c7cb3 100644 --- a/libs/types/src/Template/TemplateArgumentNode.php +++ b/libs/types/src/Template/TemplateArgumentNode.php @@ -4,20 +4,41 @@ namespace TypeLang\Type\Template; -use TypeLang\Type\Attribute\AttributeGroupListNode; use TypeLang\Type\Identifier; use TypeLang\Type\Node; use TypeLang\Type\TypeNode; +/** + * A single template argument, along with the hint it may carry. + * + * ``` + * Some\Any + * // ^^^ simple argument + * // ^^^^^^^^^ a hinted argument + * ``` + */ final class TemplateArgumentNode extends Node { - public ?Identifier $hint; - + /** + * @param int<0, max> $offset + */ public function __construct( public TypeNode $value, - ?Identifier $hint = null, - public ?AttributeGroupListNode $attributes = null, + /** + * Word standing in front of the argument, or {@see null} in case of it + * carries none. + * + * What the word means is left to whoever reads the tree. A `covariant` + * or an `out` is an ordinary identifier as far as the grammar goes. + * + * ``` + * Some\Any + * // ^^^^^^^^^ the hint + * ``` + */ + public ?Identifier $hint = null, + int $offset = 0, ) { - $this->hint = $hint; + parent::__construct($offset); } } diff --git a/libs/types/src/Template/TemplateBoundEdgeNode.php b/libs/types/src/Template/TemplateBoundEdgeNode.php new file mode 100644 index 0000000..b647e14 --- /dev/null +++ b/libs/types/src/Template/TemplateBoundEdgeNode.php @@ -0,0 +1,33 @@ +(T): T + * // ^^^^^^^ the upper bound + * // ^^^^^^^^^ the lower bound + * ``` + */ +final class TemplateBoundEdgeNode extends Node +{ + /** + * @param int<0, max> $offset + */ + public function __construct( + public Identifier $operator, + public TypeNode $type, + int $offset = 0, + ) { + parent::__construct($offset); + } +} diff --git a/libs/types/src/Template/TemplateParameterListNode.php b/libs/types/src/Template/TemplateParameterListNode.php new file mode 100644 index 0000000..cc72249 --- /dev/null +++ b/libs/types/src/Template/TemplateParameterListNode.php @@ -0,0 +1,20 @@ +(T): U + * // ^ ^^^^^^^^^ + * // two parameters + * ``` + * + * @template-extends NodeList + */ +final class TemplateParameterListNode extends NodeList {} diff --git a/libs/types/src/Template/TemplateParameterNode.php b/libs/types/src/Template/TemplateParameterNode.php new file mode 100644 index 0000000..5b04fee --- /dev/null +++ b/libs/types/src/Template/TemplateParameterNode.php @@ -0,0 +1,58 @@ +(T): U + * // ^ a name and nothing else + * // ^^^^^^^^^ the upper bound + * // ^^^^^^^^^ the lower bound + * // ^^^^^ the default + * ``` + */ +final class TemplateParameterNode extends Node +{ + /** + * @param int<0, max> $offset + */ + public function __construct( + public Identifier $name, + /** + * The end the parameter is bounded at from above, written with an + * `of` or an `as`. + * + * The argument is to be a subtype of it. + */ + public ?TemplateBoundEdgeNode $upper = null, + /** + * The end the parameter is bounded at from below, written with + * a `super`. + * + * The argument is to be a supertype of it. + */ + public ?TemplateBoundEdgeNode $lower = null, + /** + * The type the parameter takes when no argument is passed, written + * with an `=`. + * + * It bounds nothing. A bound says what an argument may be, while + * a default says what it becomes when left unsaid. + */ + public ?TypeNode $default = null, + int $offset = 0, + ) { + parent::__construct($offset); + } +} diff --git a/libs/types/src/TernaryExpressionNode.php b/libs/types/src/TernaryExpressionNode.php index 8aba2d9..bb6a400 100644 --- a/libs/types/src/TernaryExpressionNode.php +++ b/libs/types/src/TernaryExpressionNode.php @@ -6,11 +6,27 @@ use TypeLang\Type\Condition\Condition; +/** + * A type chosen by a condition. + * + * ``` + * ($value is int ? string : bool) + * ^^^^^^^^^^^^^ the condition + * ^^^^^^ the type it holds for + * ^^^^ the type it does not + * ``` + */ final class TernaryExpressionNode extends TypeNode { + /** + * @param int<0, max> $offset + */ public function __construct( public Condition $condition, public TypeNode $then, public TypeNode $else, - ) {} + int $offset = 0, + ) { + parent::__construct($offset); + } } diff --git a/libs/types/src/ThisNode.php b/libs/types/src/ThisNode.php new file mode 100644 index 0000000..e185079 --- /dev/null +++ b/libs/types/src/ThisNode.php @@ -0,0 +1,19 @@ + @@ -13,11 +21,18 @@ final class TypeOffsetAccessNode extends WrappingTypeNode { /** * @param T $type + * @param int<0, max> $offset */ public function __construct( TypeNode $type, - public readonly TypeNode $access, + /** + * The property is named an `$access` rather than an `$offset` so that + * it is not taken for the place in the source text every node + * carries. + */ + public TypeNode $access, + int $offset = 0, ) { - parent::__construct($type); + parent::__construct($type, $offset); } } diff --git a/libs/types/src/TypesListNode.php b/libs/types/src/TypesListNode.php index 013ea17..721b3a3 100644 --- a/libs/types/src/TypesListNode.php +++ b/libs/types/src/TypesListNode.php @@ -5,6 +5,16 @@ namespace TypeLang\Type; /** + * A list of the wrapped type, written in the legacy `[]` notation. + * + * ``` + * Some\Any[] + * ^^^^^^^^ the type the list is made of + * + * Some\Any[][] + * ^^^^^^^^^^ a list the outer list is made of + * ``` + * * @template T of TypeNode = TypeNode * * @template-extends WrappingTypeNode diff --git a/libs/types/src/UnionTypeNode.php b/libs/types/src/UnionTypeNode.php index 2a2deba..5bf3307 100644 --- a/libs/types/src/UnionTypeNode.php +++ b/libs/types/src/UnionTypeNode.php @@ -5,6 +5,12 @@ namespace TypeLang\Type; /** + * Any one of several types. + * + * ``` + * int|string|null + * ``` + * * @template T of TypeNode = TypeNode * * @template-extends LogicalTypeNode diff --git a/libs/types/src/VariableNode.php b/libs/types/src/VariableNode.php new file mode 100644 index 0000000..94ce8ed --- /dev/null +++ b/libs/types/src/VariableNode.php @@ -0,0 +1,32 @@ + $offset + */ + public function __construct( + /** + * The name of the variable, written without the leading `$`. + */ + public Identifier $name, + int $offset = 0, + ) { + parent::__construct($offset); + } +} diff --git a/libs/types/src/WildcardNode.php b/libs/types/src/WildcardNode.php new file mode 100644 index 0000000..0ca40cc --- /dev/null +++ b/libs/types/src/WildcardNode.php @@ -0,0 +1,40 @@ + + * // ^ an unspecified template argument + * + * Some\Any::CONST_* + * // ^ an unspecified part of a constant name + * ``` + */ +final class WildcardNode extends TypeNode implements \Stringable +{ + /** + * @var non-empty-string + */ + public const CHAR = '*'; + + /** + * @return non-empty-string + */ + public function toString(): string + { + return self::CHAR; + } + + /** + * @return non-empty-string + */ + public function __toString(): string + { + return self::CHAR; + } +} diff --git a/libs/types/src/WrappingTypeNode.php b/libs/types/src/WrappingTypeNode.php index 71c4cc0..ce58364 100644 --- a/libs/types/src/WrappingTypeNode.php +++ b/libs/types/src/WrappingTypeNode.php @@ -5,14 +5,26 @@ namespace TypeLang\Type; /** + * A type (monad) built around another one. + * + * ``` + * ?Some\Any + * ^ the wrapping type + * ^^^^^^^^ the type it wraps + * ``` + * * @template T of TypeNode = TypeNode */ abstract class WrappingTypeNode extends TypeNode { /** * @param T $type + * @param int<0, max> $offset */ public function __construct( public TypeNode $type, - ) {} + int $offset = 0, + ) { + parent::__construct($offset); + } } diff --git a/libs/types/tests/Attribute/AttributeArgumentNodeTest.php b/libs/types/tests/Attribute/AttributeArgumentNodeTest.php deleted file mode 100644 index 0b0385f..0000000 --- a/libs/types/tests/Attribute/AttributeArgumentNodeTest.php +++ /dev/null @@ -1,50 +0,0 @@ -value); - } - - #[Test] - public function attributesDefaultToNull(): void - { - $node = new AttributeArgumentNode(new NamedTypeNode(Name::createFromString('int'))); - - self::assertNull($node->attributes); - } - - #[Test] - public function constructorStoresAttributes(): void - { - $attrs = new AttributeGroupListNode(); - $value = new NamedTypeNode(Name::createFromString('int')); - $node = new AttributeArgumentNode($value, $attrs); - - self::assertSame($attrs, $node->attributes); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $node = new AttributeArgumentNode(new NamedTypeNode(Name::createFromString('int'))); - - self::assertSame(0, $node->offset); - } -} diff --git a/libs/types/tests/Attribute/AttributeArgumentsListNodeTest.php b/libs/types/tests/Attribute/AttributeArgumentsListNodeTest.php deleted file mode 100644 index 6d6c4fd..0000000 --- a/libs/types/tests/Attribute/AttributeArgumentsListNodeTest.php +++ /dev/null @@ -1,131 +0,0 @@ -first); - self::assertNull($list->last); - } - - #[Test] - public function constructorAcceptsItems(): void - { - $a = $this->makeArg('string'); - $b = $this->makeArg('int'); - $list = new AttributeArgumentListNode([$a, $b]); - - self::assertCount(2, $list); - } - - #[Test] - public function firstAndLastProperties(): void - { - $a = $this->makeArg('string'); - $b = $this->makeArg('int'); - $list = new AttributeArgumentListNode([$a, $b]); - - self::assertSame($a, $list->first); - self::assertSame($b, $list->last); - } - - #[Test] - public function arrayAccessOffsetExists(): void - { - $list = new AttributeArgumentListNode([$this->makeArg('int')]); - - self::assertTrue(isset($list[0])); - self::assertFalse(isset($list[1])); - } - - #[Test] - public function arrayAccessOffsetGet(): void - { - $arg = $this->makeArg('string'); - $list = new AttributeArgumentListNode([$arg]); - - self::assertSame($arg, $list[0]); - self::assertNull($list[1]); - } - - #[Test] - public function arrayAccessOffsetSet(): void - { - $a = $this->makeArg('string'); - $b = $this->makeArg('int'); - $list = new AttributeArgumentListNode([$a]); - $list[0] = $b; - - self::assertSame($b, $list[0]); - } - - #[Test] - public function arrayAccessOffsetUnsetReindexes(): void - { - $a = $this->makeArg('string'); - $b = $this->makeArg('int'); - $list = new AttributeArgumentListNode([$a, $b]); - unset($list[0]); - - self::assertCount(1, $list); - self::assertSame($b, $list[0]); - } - - #[Test] - public function findIndexReturnsCorrectPosition(): void - { - $a = $this->makeArg('string'); - $b = $this->makeArg('int'); - $list = new AttributeArgumentListNode([$a, $b]); - - self::assertSame(0, $list->findIndex($a)); - self::assertSame(1, $list->findIndex($b)); - } - - #[Test] - public function findIndexReturnsNullForAbsentNode(): void - { - $list = new AttributeArgumentListNode([$this->makeArg('int')]); - $absent = $this->makeArg('string'); - - self::assertNull($list->findIndex($absent)); - } - - #[Test] - public function iteratorYieldsItems(): void - { - $a = $this->makeArg('string'); - $b = $this->makeArg('int'); - $list = new AttributeArgumentListNode([$a, $b]); - - self::assertSame([$a, $b], \iterator_to_array($list)); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $list = new AttributeArgumentListNode(); - - self::assertSame(0, $list->offset); - } -} diff --git a/libs/types/tests/Attribute/AttributeGroupNodeTest.php b/libs/types/tests/Attribute/AttributeGroupNodeTest.php deleted file mode 100644 index ae1d731..0000000 --- a/libs/types/tests/Attribute/AttributeGroupNodeTest.php +++ /dev/null @@ -1,78 +0,0 @@ -first); - self::assertNull($group->last); - } - - #[Test] - public function constructorAcceptsAttributes(): void - { - $a = $this->makeAttr('Pure'); - $b = $this->makeAttr('Deprecated'); - $group = new AttributeGroupNode([$a, $b]); - - self::assertCount(2, $group); - self::assertSame($a, $group->first); - self::assertSame($b, $group->last); - } - - #[Test] - public function findIndexReturnsPosition(): void - { - $a = $this->makeAttr('Pure'); - $b = $this->makeAttr('Deprecated'); - $group = new AttributeGroupNode([$a, $b]); - - self::assertSame(0, $group->findIndex($a)); - self::assertSame(1, $group->findIndex($b)); - } - - #[Test] - public function findIndexReturnsNullWhenNotFound(): void - { - $group = new AttributeGroupNode([$this->makeAttr('Pure')]); - - self::assertNull($group->findIndex($this->makeAttr('Other'))); - } - - #[Test] - public function iteratorYieldsAttributes(): void - { - $a = $this->makeAttr('A'); - $b = $this->makeAttr('B'); - $group = new AttributeGroupNode([$a, $b]); - - self::assertSame([$a, $b], \iterator_to_array($group)); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $group = new AttributeGroupNode(); - - self::assertSame(0, $group->offset); - } -} diff --git a/libs/types/tests/Attribute/AttributeGroupsListNodeTest.php b/libs/types/tests/Attribute/AttributeGroupsListNodeTest.php deleted file mode 100644 index 40ae29c..0000000 --- a/libs/types/tests/Attribute/AttributeGroupsListNodeTest.php +++ /dev/null @@ -1,78 +0,0 @@ - new AttributeNode(Name::createFromString($n)), - $names, - ); - - return new AttributeGroupNode($attrs); - } - - #[Test] - public function emptyListByDefault(): void - { - $list = new AttributeGroupListNode(); - - self::assertCount(0, $list); - self::assertNull($list->first); - self::assertNull($list->last); - } - - #[Test] - public function constructorAcceptsGroups(): void - { - $g1 = $this->makeGroup('Pure'); - $g2 = $this->makeGroup('Deprecated'); - $list = new AttributeGroupListNode([$g1, $g2]); - - self::assertCount(2, $list); - self::assertSame($g1, $list->first); - self::assertSame($g2, $list->last); - } - - #[Test] - public function arrayAccessWorks(): void - { - $g = $this->makeGroup('Pure'); - $list = new AttributeGroupListNode([$g]); - - self::assertTrue(isset($list[0])); - self::assertSame($g, $list[0]); - self::assertNull($list[1]); - } - - #[Test] - public function unsetRemovesAndReindexes(): void - { - $g1 = $this->makeGroup('A'); - $g2 = $this->makeGroup('B'); - $list = new AttributeGroupListNode([$g1, $g2]); - unset($list[0]); - - self::assertCount(1, $list); - self::assertSame($g2, $list[0]); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $list = new AttributeGroupListNode(); - - self::assertSame(0, $list->offset); - } -} diff --git a/libs/types/tests/Attribute/AttributeNodeTest.php b/libs/types/tests/Attribute/AttributeNodeTest.php deleted file mode 100644 index 114faa5..0000000 --- a/libs/types/tests/Attribute/AttributeNodeTest.php +++ /dev/null @@ -1,48 +0,0 @@ -name); - } - - #[Test] - public function argumentsDefaultToNull(): void - { - $node = new AttributeNode(Name::createFromString('Pure')); - - self::assertNull($node->arguments); - } - - #[Test] - public function constructorStoresArguments(): void - { - $args = new AttributeArgumentListNode(); - $node = new AttributeNode(Name::createFromString('Attr'), $args); - - self::assertSame($args, $node->arguments); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $node = new AttributeNode(Name::createFromString('Attr')); - - self::assertSame(0, $node->offset); - } -} diff --git a/libs/types/tests/Callable/CallableParameterNodeTest.php b/libs/types/tests/Callable/CallableParameterNodeTest.php index 6b97baf..7e94da0 100644 --- a/libs/types/tests/Callable/CallableParameterNodeTest.php +++ b/libs/types/tests/Callable/CallableParameterNodeTest.php @@ -6,9 +6,11 @@ use PHPUnit\Framework\Attributes\Test; use TypeLang\Type\Callable\CallableParameterNode; -use TypeLang\Type\Literal\VariableLiteralNode; +use TypeLang\Type\Identifier; +use TypeLang\Type\VariableNode; use TypeLang\Type\Name; use TypeLang\Type\NamedTypeNode; +use TypeLang\Type\Node; use TypeLang\Type\Tests\TestCase; final class CallableParameterNodeTest extends TestCase @@ -24,13 +26,12 @@ public function constructorWithTypeOnly(): void self::assertFalse($node->isOutput); self::assertFalse($node->isVariadic); self::assertFalse($node->isOptional); - self::assertNull($node->attributes); } #[Test] public function constructorWithNameOnly(): void { - $name = VariableLiteralNode::parse('param'); + $name = new VariableNode(new Identifier('param')); $node = new CallableParameterNode(name: $name); self::assertNull($node->type); @@ -41,7 +42,7 @@ public function constructorWithNameOnly(): void public function constructorWithTypeAndName(): void { $type = new NamedTypeNode(Name::createFromString('int')); - $name = VariableLiteralNode::parse('count'); + $name = new VariableNode(new Identifier('count')); $node = new CallableParameterNode($type, $name); self::assertSame($type, $node->type); @@ -81,59 +82,6 @@ public function optionalFlagIsStored(): void self::assertTrue($node->isOptional); } - #[Test] - public function toStringReturnsSimpleWhenNoFlags(): void - { - $node = new CallableParameterNode(type: new NamedTypeNode(Name::createFromString('int'))); - - self::assertSame('simple', (string) $node); - } - - #[Test] - public function toStringReturnsOutputWhenOutputIsSet(): void - { - $node = new CallableParameterNode( - type: new NamedTypeNode(Name::createFromString('int')), - isOutput: true, - ); - - self::assertSame('output', (string) $node); - } - - #[Test] - public function toStringReturnsVariadicWhenVariadicIsSet(): void - { - $node = new CallableParameterNode( - type: new NamedTypeNode(Name::createFromString('int')), - isVariadic: true, - ); - - self::assertSame('variadic', (string) $node); - } - - #[Test] - public function toStringReturnsOptionalWhenOptionalIsSet(): void - { - $node = new CallableParameterNode( - type: new NamedTypeNode(Name::createFromString('int')), - isOptional: true, - ); - - self::assertSame('optional', (string) $node); - } - - #[Test] - public function toStringCombinesMultipleFlags(): void - { - $node = new CallableParameterNode( - type: new NamedTypeNode(Name::createFromString('int')), - isOutput: true, - isOptional: true, - ); - - self::assertSame('output, optional', (string) $node); - } - #[Test] public function throwsWhenBothTypeAndNameAreNull(): void { @@ -165,4 +113,21 @@ public function defaultOffsetIsZero(): void self::assertSame(0, $node->offset); } + + #[Test] + public function isReturnsTrueForOwnClass(): void + { + $parameter = new CallableParameterNode(new NamedTypeNode(Name::createFromString('int'))); + + self::assertTrue($parameter->is(CallableParameterNode::class)); + self::assertTrue($parameter->is(Node::class)); + } + + #[Test] + public function isReturnsFalseForAnotherClass(): void + { + $parameter = new CallableParameterNode(new NamedTypeNode(Name::createFromString('int'))); + + self::assertFalse($parameter->is(NamedTypeNode::class)); + } } diff --git a/libs/types/tests/Callable/CallableParametersListNodeTest.php b/libs/types/tests/Callable/CallableParametersListNodeTest.php index 06559e6..7eb1a13 100644 --- a/libs/types/tests/Callable/CallableParametersListNodeTest.php +++ b/libs/types/tests/Callable/CallableParametersListNodeTest.php @@ -24,8 +24,8 @@ public function emptyListByDefault(): void $list = new CallableParameterListNode(); self::assertCount(0, $list); - self::assertNull($list->first); - self::assertNull($list->last); + self::assertNull($list->first()); + self::assertNull($list->last()); } #[Test] @@ -36,8 +36,8 @@ public function constructorAcceptsParameters(): void $list = new CallableParameterListNode([$a, $b]); self::assertCount(2, $list); - self::assertSame($a, $list->first); - self::assertSame($b, $list->last); + self::assertSame($a, $list->first()); + self::assertSame($b, $list->last()); } #[Test] diff --git a/libs/types/tests/ClassConstMaskNodeTest.php b/libs/types/tests/ClassConstMaskNodeTest.php index 33268cc..bef275f 100644 --- a/libs/types/tests/ClassConstMaskNodeTest.php +++ b/libs/types/tests/ClassConstMaskNodeTest.php @@ -7,44 +7,42 @@ use PHPUnit\Framework\Attributes\Test; use TypeLang\Type\ClassConstMaskNode; use TypeLang\Type\Identifier; +use TypeLang\Type\MaskNode; use TypeLang\Type\Name; +use TypeLang\Type\WildcardNode; final class ClassConstMaskNodeTest extends TestCase { #[Test] - public function constructorStoresClassWithConstant(): void + public function constructorStoresClassWithMask(): void { $class = Name::createFromString('MyClass'); - $const = new Identifier('STATUS_'); - $node = new ClassConstMaskNode($class, $const); + $mask = new MaskNode([new Identifier('STATUS_'), new WildcardNode()]); + $node = new ClassConstMaskNode($class, $mask); self::assertSame($class, $node->class); - self::assertSame($const, $node->constant); + self::assertSame($mask, $node->mask); } #[Test] - public function constantDefaultsToNull(): void + public function aMaskOfNothingButAWildcardCarriesNoSegments(): void { - $class = Name::createFromString('MyClass'); - $node = new ClassConstMaskNode($class); - - self::assertNull($node->constant); - } + $node = new ClassConstMaskNode( + Name::createFromString('MyEnum'), + new MaskNode([new WildcardNode()]), + ); - #[Test] - public function constructorAcceptsNullConstant(): void - { - $class = Name::createFromString('MyEnum'); - $node = new ClassConstMaskNode($class, null); - - self::assertSame($class, $node->class); - self::assertNull($node->constant); + self::assertSame([], $node->mask->getSegments()); + self::assertSame('*', $node->mask->toString()); } #[Test] public function defaultOffsetIsZero(): void { - $node = new ClassConstMaskNode(Name::createFromString('Foo')); + $node = new ClassConstMaskNode( + Name::createFromString('Foo'), + new MaskNode([new WildcardNode()]), + ); self::assertSame(0, $node->offset); } diff --git a/libs/types/tests/Condition/GreaterThanConditionNodeTest.php b/libs/types/tests/Condition/GreaterThanConditionNodeTest.php deleted file mode 100644 index c3931fa..0000000 --- a/libs/types/tests/Condition/GreaterThanConditionNodeTest.php +++ /dev/null @@ -1,36 +0,0 @@ -subject); - self::assertSame($target, $node->target); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $node = new GreaterThanConditionNode( - new NamedTypeNode(Name::createFromString('T')), - new NamedTypeNode(Name::createFromString('int')), - ); - - self::assertSame(0, $node->offset); - } -} diff --git a/libs/types/tests/Condition/GreaterThanOrEqualConditionNodeTest.php b/libs/types/tests/Condition/GreaterThanOrEqualConditionNodeTest.php deleted file mode 100644 index 619ba02..0000000 --- a/libs/types/tests/Condition/GreaterThanOrEqualConditionNodeTest.php +++ /dev/null @@ -1,36 +0,0 @@ -subject); - self::assertSame($target, $node->target); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $node = new GreaterThanOrEqualConditionNode( - new NamedTypeNode(Name::createFromString('T')), - new NamedTypeNode(Name::createFromString('int')), - ); - - self::assertSame(0, $node->offset); - } -} diff --git a/libs/types/tests/Condition/LessThanConditionNodeTest.php b/libs/types/tests/Condition/LessThanConditionNodeTest.php deleted file mode 100644 index 81f23ca..0000000 --- a/libs/types/tests/Condition/LessThanConditionNodeTest.php +++ /dev/null @@ -1,36 +0,0 @@ -subject); - self::assertSame($target, $node->target); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $node = new LessThanConditionNode( - new NamedTypeNode(Name::createFromString('T')), - new NamedTypeNode(Name::createFromString('int')), - ); - - self::assertSame(0, $node->offset); - } -} diff --git a/libs/types/tests/Condition/LessThanOrEqualConditionNodeTest.php b/libs/types/tests/Condition/LessThanOrEqualConditionNodeTest.php deleted file mode 100644 index 642eb36..0000000 --- a/libs/types/tests/Condition/LessThanOrEqualConditionNodeTest.php +++ /dev/null @@ -1,36 +0,0 @@ -subject); - self::assertSame($target, $node->target); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $node = new LessThanOrEqualConditionNode( - new NamedTypeNode(Name::createFromString('T')), - new NamedTypeNode(Name::createFromString('int')), - ); - - self::assertSame(0, $node->offset); - } -} diff --git a/libs/types/tests/ConstMaskNodeTest.php b/libs/types/tests/ConstMaskNodeTest.php index 6c1d56a..8892e16 100644 --- a/libs/types/tests/ConstMaskNodeTest.php +++ b/libs/types/tests/ConstMaskNodeTest.php @@ -6,40 +6,137 @@ use PHPUnit\Framework\Attributes\Test; use TypeLang\Type\ConstMaskNode; +use TypeLang\Type\Identifier; +use TypeLang\Type\MaskNode; use TypeLang\Type\Name; +use TypeLang\Type\WildcardNode; final class ConstMaskNodeTest extends TestCase { #[Test] - public function constructorStoresName(): void + public function constructorStoresTheMask(): void { - $name = Name::createFromString('SOME_CONST'); - $node = new ConstMaskNode($name); + $mask = new MaskNode([new Identifier('SOME_CONST'), new WildcardNode()]); + $node = new ConstMaskNode($mask); - self::assertSame($name, $node->name); + self::assertSame($mask, $node->mask); } #[Test] - public function toStringAppendsAsterisk(): void + public function aMaskIsWrittenWithNoNamespaceByDefault(): void { - $node = new ConstMaskNode(Name::createFromString('SOME_CONST')); + $node = new ConstMaskNode(new MaskNode([new WildcardNode(), new Identifier('_SOME')])); - self::assertSame('SOME_CONST*', (string) $node); + self::assertFalse($node->namespaceOrFullyQualified); + self::assertFalse($node->isFullyQualified()); } #[Test] - public function toStringWithQualifiedNameAppendsAsterisk(): void + public function constructorStoresTheNamespace(): void { - $node = new ConstMaskNode(Name::createFromString('Vendor\Package\STATUS')); + $namespace = Name::createFromString('Some\Any'); + $node = new ConstMaskNode( + new MaskNode([new Identifier('SOME_'), new WildcardNode()]), + $namespace, + ); - self::assertSame('Vendor\Package\STATUS*', (string) $node); + self::assertSame($namespace, $node->namespaceOrFullyQualified); + self::assertFalse($node->isFullyQualified()); + } + + /** + * The leading separator belongs to the namespace, so the namespace is the + * one that says whether the reference is a fully qualified one. + */ + #[Test] + public function theNamespaceSaysTheReferenceIsFullyQualified(): void + { + $node = new ConstMaskNode( + new MaskNode([new Identifier('SOME_'), new WildcardNode()]), + Name::createFromString('\Some\Any'), + ); + + self::assertTrue($node->isFullyQualified()); + } + + /** + * A mask written with no namespace has nothing to carry that separator, + * so it is passed on its own. + */ + #[Test] + public function aMaskWithNoNamespaceIsFullyQualifiedOnItsOwn(): void + { + $node = new ConstMaskNode( + new MaskNode([new Identifier('SOME_'), new WildcardNode()]), + true, + ); + + self::assertTrue($node->namespaceOrFullyQualified); + self::assertTrue($node->isFullyQualified()); + } + + #[Test] + public function theSeparatorIsSetOnTheNamespaceItIsWrittenWith(): void + { + $node = new ConstMaskNode( + new MaskNode([new Identifier('SOME_'), new WildcardNode()]), + Name::createFromString('Some\Any'), + ); + + $node->setFullyQualified(); + + self::assertTrue($node->isFullyQualified()); + self::assertInstanceOf(Name::class, $node->namespaceOrFullyQualified); + self::assertSame('\Some\Any', $node->namespaceOrFullyQualified->toString()); + } + + #[Test] + public function theSeparatorIsTakenOffTheNamespaceItIsWrittenWith(): void + { + $node = new ConstMaskNode( + new MaskNode([new Identifier('SOME_'), new WildcardNode()]), + Name::createFromString('\Some\Any'), + ); + + $node->setFullyQualified(false); + + self::assertFalse($node->isFullyQualified()); + self::assertInstanceOf(Name::class, $node->namespaceOrFullyQualified); + self::assertSame('Some\Any', $node->namespaceOrFullyQualified->toString()); + } + + #[Test] + public function theSeparatorIsSetOnAMaskWrittenWithNoNamespace(): void + { + $node = new ConstMaskNode(new MaskNode([new Identifier('SOME_'), new WildcardNode()])); + + $node->setFullyQualified(); + + self::assertTrue($node->namespaceOrFullyQualified); + self::assertTrue($node->isFullyQualified()); } #[Test] public function defaultOffsetIsZero(): void { - $node = new ConstMaskNode(Name::createFromString('FOO')); + $node = new ConstMaskNode(new MaskNode([new Identifier('FOO'), new WildcardNode()])); self::assertSame(0, $node->offset); } + + #[Test] + public function aMaskIsWrittenTheWayItIsRead(): void + { + $node = new ConstMaskNode( + new MaskNode([ + new Identifier('SOME'), + new WildcardNode(), + new Identifier('ANY'), + new WildcardNode(), + ]), + ); + + self::assertSame('SOME*ANY*', $node->mask->toString()); + self::assertSame(['SOME', 'ANY'], $node->mask->getSegmentsAsStrings()); + } } diff --git a/libs/types/tests/IdentifierTest.php b/libs/types/tests/IdentifierTest.php index d093dd4..0941629 100644 --- a/libs/types/tests/IdentifierTest.php +++ b/libs/types/tests/IdentifierTest.php @@ -48,7 +48,7 @@ public function isVirtualIsTrueForHyphenatedName(): void { $id = new Identifier('non-empty-string'); - self::assertTrue($id->isVirtual); + self::assertTrue($id->isVirtual()); } #[Test] @@ -56,7 +56,7 @@ public function isVirtualIsFalseForNormalName(): void { $id = new Identifier('string'); - self::assertFalse($id->isVirtual); + self::assertFalse($id->isVirtual()); } #[Test] @@ -65,7 +65,7 @@ public function isSpecialIsTrueForSpecialClassNames(string $name): void { $id = new Identifier($name); - self::assertTrue($id->isSpecial); + self::assertTrue($id->isSpecial()); } public static function provideSpecialNames(): iterable @@ -78,7 +78,7 @@ public function isSpecialIsFalseForRegularName(): void { $id = new Identifier('MyClass'); - self::assertFalse($id->isSpecial); + self::assertFalse($id->isSpecial()); } #[Test] @@ -87,7 +87,7 @@ public function isBuiltinIsTrueForBuiltinTypes(string $name): void { $id = new Identifier($name); - self::assertTrue($id->isBuiltin); + self::assertTrue($id->isBuiltin()); } public static function provideBuiltinNames(): iterable @@ -105,7 +105,7 @@ public function isBuiltinIsFalseForCustomName(): void { $id = new Identifier('MyClass'); - self::assertFalse($id->isBuiltin); + self::assertFalse($id->isBuiltin()); } #[Test] @@ -192,4 +192,170 @@ public function serializeAndUnserializeRoundtrip(): void self::assertSame('MyClass', $restored->value); self::assertSame(42, $restored->offset); } + + #[Test] + public function specialNamesAreCaseInsensitive(): void + { + self::assertTrue((new Identifier('SELF'))->isSpecial()); + self::assertTrue((new Identifier('Parent'))->isSpecial()); + self::assertTrue(Identifier::isLooksLikeSpecial('StAtIc')); + } + + #[Test] + public function builtinNamesAreCaseInsensitive(): void + { + self::assertTrue((new Identifier('INT'))->isBuiltin()); + self::assertTrue((new Identifier('Iterable'))->isBuiltin()); + self::assertTrue(Identifier::isLooksLikeBuiltin('NuLl')); + } + + #[Test] + public function specialNameIsNotBuiltin(): void + { + $id = new Identifier('self'); + + self::assertTrue($id->isSpecial()); + self::assertFalse($id->isBuiltin()); + } + + #[Test] + public function builtinNameIsNotSpecial(): void + { + $id = new Identifier('string'); + + self::assertTrue($id->isBuiltin()); + self::assertFalse($id->isSpecial()); + } + + #[Test] + public function virtualNameIsDetectedInAnyPosition(): void + { + self::assertTrue((new Identifier('-leading'))->isVirtual()); + self::assertTrue((new Identifier('trailing-'))->isVirtual()); + self::assertTrue((new Identifier('a-b-c'))->isVirtual()); + } + + #[Test] + public function underscoredNameIsNotVirtual(): void + { + self::assertFalse((new Identifier('non_empty_string'))->isVirtual()); + } + + #[Test] + public function createFromStringAcceptsStringableObject(): void + { + $stringable = new class implements \Stringable { + public function __toString(): string + { + return 'Example'; + } + }; + + self::assertSame('Example', Identifier::createFromString($stringable)->value); + } + + #[Test] + public function createFromStringTrimsAllSurroundingWhitespaceKinds(): void + { + self::assertSame('Example', Identifier::createFromString("\n\t Example \r\n")->value); + } + + #[Test] + public function createFromStringThrowsOnStringableReturningEmptyString(): void + { + $stringable = new class implements \Stringable { + public function __toString(): string + { + return ' '; + } + }; + + $this->expectException(\InvalidArgumentException::class); + + Identifier::createFromString($stringable); + } + + #[Test] + public function offsetMethodIsAnAliasOfProperty(): void + { + $id = new Identifier('Example'); + $id->offset = 17; + + self::assertSame(17, $id->offset); + } + + #[Test] + public function serializePayloadContainsOffsetAndValue(): void + { + $id = new Identifier('Example'); + $id->offset = 5; + + self::assertSame([5, 'Example'], $id->__serialize()); + } + + #[Test] + public function serializationRoundtripPreservesOffset(): void + { + $id = new Identifier('Example'); + $id->offset = 5; + + /** @var Identifier $restored */ + $restored = \unserialize(\serialize($id)); + + self::assertSame(5, $restored->offset); + self::assertSame('Example', $restored->value); + } + + #[Test] + public function unserializeThrowsWhenOffsetIsMissing(): void + { + $id = new Identifier('Example'); + + $this->expectException(\UnexpectedValueException::class); + + $id->__unserialize([]); + } + + #[Test] + public function unserializeThrowsWhenValueIsMissing(): void + { + $id = new Identifier('Example'); + + $this->expectException(\UnexpectedValueException::class); + + $id->__unserialize([0]); + } + + #[Test] + public function stringCastIsTheSameAsToStringMethod(): void + { + $id = new Identifier('Example'); + + self::assertSame($id->toString(), (string) $id); + } + + #[Test] + public function toLowerStringDoesNotModifyTheValue(): void + { + $id = new Identifier('SomeClass'); + + self::assertSame('someclass', $id->toLowerString()); + self::assertSame('SomeClass', $id->value); + } + + #[Test] + public function constructorThrowsOnEmptyValue(): void + { + $this->expectException(\InvalidArgumentException::class); + + new Identifier(''); + } + + #[Test] + public function constructorKeepsSurroundingWhitespace(): void + { + $id = new Identifier(' A '); + + self::assertSame(' A ', $id->value); + } } diff --git a/libs/types/tests/IntersectionTypeNodeTest.php b/libs/types/tests/IntersectionTypeNodeTest.php index 6c6e2af..4332b02 100644 --- a/libs/types/tests/IntersectionTypeNodeTest.php +++ b/libs/types/tests/IntersectionTypeNodeTest.php @@ -21,7 +21,7 @@ public function constructorWithTwoTypes(): void { $a = $this->named('Countable'); $b = $this->named('Stringable'); - $node = new IntersectionTypeNode($a, $b); + $node = new IntersectionTypeNode([$a, $b]); self::assertCount(2, $node); self::assertSame([$a, $b], $node->statements); @@ -33,7 +33,7 @@ public function constructorWithThreeTypes(): void $a = $this->named('A'); $b = $this->named('B'); $c = $this->named('C'); - $node = new IntersectionTypeNode($a, $b, $c); + $node = new IntersectionTypeNode([$a, $b, $c]); self::assertCount(3, $node); } @@ -45,8 +45,8 @@ public function nestedIntersectionIsFlattened(): void $b = $this->named('B'); $c = $this->named('C'); - $inner = new IntersectionTypeNode($a, $b); - $outer = new IntersectionTypeNode($inner, $c); + $inner = new IntersectionTypeNode([$a, $b]); + $outer = new IntersectionTypeNode([$inner, $c]); self::assertCount(3, $outer); self::assertSame([$a, $b, $c], $outer->statements); @@ -57,7 +57,7 @@ public function iteratorYieldsStatements(): void { $a = $this->named('Countable'); $b = $this->named('Stringable'); - $node = new IntersectionTypeNode($a, $b); + $node = new IntersectionTypeNode([$a, $b]); self::assertSame([$a, $b], \iterator_to_array($node)); } @@ -65,7 +65,7 @@ public function iteratorYieldsStatements(): void #[Test] public function defaultOffsetIsZero(): void { - $node = new IntersectionTypeNode($this->named('A'), $this->named('B')); + $node = new IntersectionTypeNode([$this->named('A'), $this->named('B')]); self::assertSame(0, $node->offset); } diff --git a/libs/types/tests/Literal/BoolLiteralNodeTest.php b/libs/types/tests/Literal/BoolLiteralNodeTest.php index 86c0331..ea5c55d 100644 --- a/libs/types/tests/Literal/BoolLiteralNodeTest.php +++ b/libs/types/tests/Literal/BoolLiteralNodeTest.php @@ -45,47 +45,6 @@ public function toStringReturnsRaw(): void self::assertSame('true', (string) $node); } - #[Test] - public function parseLowercaseTrue(): void - { - $node = BoolLiteralNode::parse('true'); - - self::assertTrue($node->value); - self::assertSame('true', $node->raw); - } - - #[Test] - public function parseUppercaseTrue(): void - { - $node = BoolLiteralNode::parse('TRUE'); - - self::assertTrue($node->value); - } - - #[Test] - public function parseLowercaseFalse(): void - { - $node = BoolLiteralNode::parse('false'); - - self::assertFalse($node->value); - } - - #[Test] - public function parseUppercaseFalse(): void - { - $node = BoolLiteralNode::parse('FALSE'); - - self::assertFalse($node->value); - } - - #[Test] - public function parseNonTrueStringReturnsFalse(): void - { - $node = BoolLiteralNode::parse('yes'); - - self::assertFalse($node->value); - } - #[Test] public function defaultOffsetIsZero(): void { diff --git a/libs/types/tests/Literal/FloatLiteralNodeTest.php b/libs/types/tests/Literal/FloatLiteralNodeTest.php index 4093144..042faa1 100644 --- a/libs/types/tests/Literal/FloatLiteralNodeTest.php +++ b/libs/types/tests/Literal/FloatLiteralNodeTest.php @@ -19,15 +19,6 @@ public function constructorStoresValueAndRaw(): void self::assertSame('3.14', $node->raw); } - #[Test] - public function parseDerivesRawFromValue(): void - { - $node = FloatLiteralNode::parse('1.5'); - - self::assertSame(1.5, $node->value); - self::assertSame('1.5', $node->raw); - } - #[Test] public function toStringReturnsRaw(): void { @@ -36,44 +27,10 @@ public function toStringReturnsRaw(): void self::assertSame('3.14', (string) $node); } - #[Test] - public function parseValidFloat(): void - { - $node = FloatLiteralNode::parse('3.14'); - - self::assertSame(3.14, $node->value); - self::assertSame('3.14', $node->raw); - } - - #[Test] - public function parseNegativeFloat(): void - { - $node = FloatLiteralNode::parse('-1.5'); - - self::assertSame(-1.5, $node->value); - } - - #[Test] - public function parseScientificNotation(): void - { - $node = FloatLiteralNode::parse('1.5e2'); - - self::assertSame(150.0, $node->value); - } - - #[Test] - public function parseNonNumericStringReturnsZero(): void - { - $node = FloatLiteralNode::parse('not-a-number'); - - self::assertSame(0.0, $node->value); - self::assertSame('not-a-number', $node->raw); - } - #[Test] public function defaultOffsetIsZero(): void { - $node = FloatLiteralNode::parse('0.0'); + $node = new FloatLiteralNode(0.0); self::assertSame(0, $node->offset); } diff --git a/libs/types/tests/Literal/IntLiteralNodeTest.php b/libs/types/tests/Literal/IntLiteralNodeTest.php index 6432a3a..55571be 100644 --- a/libs/types/tests/Literal/IntLiteralNodeTest.php +++ b/libs/types/tests/Literal/IntLiteralNodeTest.php @@ -4,7 +4,6 @@ namespace TypeLang\Type\Tests\Literal; -use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use TypeLang\Type\Literal\IntLiteralNode; use TypeLang\Type\Tests\TestCase; @@ -39,86 +38,10 @@ public function toStringReturnsRaw(): void self::assertSame('0x2A', (string) $node); } - #[Test] - #[DataProvider('provideDecimalIntegers')] - public function parseDecimalInteger(string $input, int $expected): void - { - $node = IntLiteralNode::parse($input); - - self::assertSame($expected, $node->value); - self::assertSame($input, $node->raw); - } - - public static function provideDecimalIntegers(): iterable - { - return [ - ['0', 0], - ['1', 1], - ['42', 42], - ['100', 100], - ]; - } - - #[Test] - public function parseNegativeInteger(): void - { - $node = IntLiteralNode::parse('-42'); - - self::assertSame(-42, $node->value); - } - - #[Test] - public function parseHexadecimalInteger(): void - { - $node = IntLiteralNode::parse('0xFF'); - - self::assertSame(255, $node->value); - } - - #[Test] - public function parseBinaryInteger(): void - { - $node = IntLiteralNode::parse('0b1010'); - - self::assertSame(10, $node->value); - } - - #[Test] - public function parseOctalInteger(): void - { - $node = IntLiteralNode::parse('0o17'); - - self::assertSame(15, $node->value); - } - - #[Test] - public function parseLegacyOctalInteger(): void - { - $node = IntLiteralNode::parse('017'); - - self::assertSame(15, $node->value); - } - - #[Test] - public function parseIntegerWithUnderscores(): void - { - $node = IntLiteralNode::parse('1_000_000'); - - self::assertSame(1000000, $node->value); - } - - #[Test] - public function parsePhpIntMin(): void - { - $node = IntLiteralNode::parse((string) \PHP_INT_MIN); - - self::assertSame(\PHP_INT_MIN, $node->value); - } - #[Test] public function defaultOffsetIsZero(): void { - $node = IntLiteralNode::parse('0'); + $node = new IntLiteralNode(0); self::assertSame(0, $node->offset); } diff --git a/libs/types/tests/Literal/LiteralNodeTest.php b/libs/types/tests/Literal/LiteralNodeTest.php new file mode 100644 index 0000000..350294f --- /dev/null +++ b/libs/types/tests/Literal/LiteralNodeTest.php @@ -0,0 +1,123 @@ + + */ + public static function provideLiterals(): iterable + { + yield 'bool(true)' => [new BoolLiteralNode(true, 'true'), true, 'true']; + yield 'bool(false)' => [new BoolLiteralNode(false, 'false'), false, 'false']; + yield 'float' => [new FloatLiteralNode(0.5, '0.5'), 0.5, '0.5']; + yield 'int' => [new IntLiteralNode(42, '42', '42'), 42, '42']; + yield 'null' => [new NullLiteralNode(), null, 'null']; + yield 'string' => [new StringLiteralNode('example', '"example"'), 'example', '"example"']; + } + + #[Test] + #[DataProvider('provideLiterals')] + public function valueMethodIsAnAliasOfValueProperty(LiteralNode $node, mixed $value, string $raw): void + { + self::assertSame($value, $node->value); + self::assertSame($value, $node->value); + } + + #[Test] + #[DataProvider('provideLiterals')] + public function rawMethodIsAnAliasOfRawProperty(LiteralNode $node, mixed $value, string $raw): void + { + self::assertSame($raw, $node->raw); + self::assertSame($raw, $node->raw); + } + + #[Test] + #[DataProvider('provideLiterals')] + public function stringRepresentationIsTheRawValue(LiteralNode $node, mixed $value, string $raw): void + { + self::assertSame($raw, (string) $node); + } + + #[Test] + #[DataProvider('provideLiterals')] + public function everyLiteralImplementsItsContracts(LiteralNode $node, mixed $value, string $raw): void + { + self::assertInstanceOf(LiteralNodeInterface::class, $node); + self::assertInstanceOf(TypeNode::class, $node); + self::assertInstanceOf(\Stringable::class, $node); + } + + #[Test] + public function boolLiteralDerivesRawFromValue(): void + { + self::assertSame('true', (new BoolLiteralNode(true))->raw); + self::assertSame('false', (new BoolLiteralNode(false))->raw); + } + + #[Test] + public function nullLiteralDerivesRawFromValue(): void + { + self::assertSame('null', (new NullLiteralNode())->raw); + self::assertNull((new NullLiteralNode())->value); + } + + #[Test] + public function stringLiteralDerivesRawFromValue(): void + { + self::assertSame("'example'", (new StringLiteralNode('example'))->raw); + } + + #[Test] + public function intLiteralDerivesRawFromValue(): void + { + $node = new IntLiteralNode(42); + + self::assertSame(42, $node->value); + self::assertSame('42', $node->raw); + self::assertSame('42', $node->decimal); + } + + #[Test] + public function negativeIntLiteralDerivesRawFromValue(): void + { + $node = new IntLiteralNode(-42); + + self::assertSame('-42', $node->raw); + self::assertSame('-42', $node->decimal); + } + + #[Test] + public function floatLiteralDerivesRawFromValue(): void + { + $node = new FloatLiteralNode(0.5); + + self::assertSame(0.5, $node->value); + self::assertSame('0.5', $node->raw); + } + + #[Test] + public function literalValueAndRawArePartOfTheNodeState(): void + { + $node = new StringLiteralNode('example'); + $node->offset = 5; + + self::assertSame(5, $node->offset); + self::assertSame('example', $node->value); + } +} diff --git a/libs/types/tests/Literal/StringLiteralNodeTest.php b/libs/types/tests/Literal/StringLiteralNodeTest.php index 4cbac41..d0e2ad6 100644 --- a/libs/types/tests/Literal/StringLiteralNodeTest.php +++ b/libs/types/tests/Literal/StringLiteralNodeTest.php @@ -25,7 +25,7 @@ public function constructorDerivesRawWhenOmitted(): void $node = new StringLiteralNode('hello'); self::assertSame('hello', $node->value); - self::assertSame('"hello"', $node->raw); + self::assertSame("'hello'", $node->raw); } #[Test] @@ -37,106 +37,19 @@ public function toStringReturnsRaw(): void } #[Test] - public function parseDoubleQuotedString(): void - { - $node = StringLiteralNode::parse('"hello"'); - - self::assertSame('hello', $node->value); - self::assertSame('"hello"', $node->raw); - } - - #[Test] - public function parseSingleQuotedString(): void - { - $node = StringLiteralNode::parse("'hello'"); - - self::assertSame('hello', $node->value); - self::assertSame("'hello'", $node->raw); - } - - #[Test] - public function parseDoubleQuotedWithEscapedQuote(): void - { - $node = StringLiteralNode::parse('"say \\"hello\\""'); - - self::assertSame('say "hello"', $node->value); - } - - #[Test] - public function parseSingleQuotedWithEscapedQuote(): void - { - $node = StringLiteralNode::parse("'it\\'s'"); - - self::assertSame("it's", $node->value); - } - - #[Test] - public function parseNewlineEscapeSequence(): void - { - $node = StringLiteralNode::parse('"line1\\nline2"'); - - self::assertSame("line1\nline2", $node->value); - } - - #[Test] - public function parseTabEscapeSequence(): void - { - $node = StringLiteralNode::parse('"col1\\tcol2"'); - - self::assertSame("col1\tcol2", $node->value); - } - - #[Test] - public function parseHexSequence(): void - { - $node = StringLiteralNode::parse('"\\x41"'); - - self::assertSame('A', $node->value); - } - - #[Test] - public function parseUnicodeSequence(): void - { - $node = StringLiteralNode::parse('"\\u{0041}"'); - - self::assertSame('A', $node->value); - } - - #[Test] - public function createFromDoubleQuotedString(): void - { - $node = StringLiteralNode::createFromDoubleQuotedString('"world"'); - - self::assertSame('world', $node->value); - } - - #[Test] - public function createFromSingleQuotedString(): void - { - $node = StringLiteralNode::createFromSingleQuotedString("'world'"); - - self::assertSame('world', $node->value); - } - - #[Test] - public function parseThrowsOnStringTooShort(): void + public function defaultOffsetIsZero(): void { - $this->expectException(\InvalidArgumentException::class); - StringLiteralNode::parse('"'); - } + $node = new StringLiteralNode('test'); - #[Test] - public function parseThrowsOnEmptyString(): void - { - $this->expectException(\InvalidArgumentException::class); - StringLiteralNode::parse(''); + self::assertSame(0, $node->offset); } #[Test] - public function defaultOffsetIsZero(): void + public function constructorEscapesTheDerivedRawValue(): void { - $node = new StringLiteralNode('test'); + $node = new StringLiteralNode("a'b"); - self::assertSame(0, $node->offset); + self::assertSame("'a\'b'", $node->raw); + self::assertSame("a'b", $node->value); } } diff --git a/libs/types/tests/Literal/VariableLiteralNodeTest.php b/libs/types/tests/Literal/VariableLiteralNodeTest.php deleted file mode 100644 index 9c06f80..0000000 --- a/libs/types/tests/Literal/VariableLiteralNodeTest.php +++ /dev/null @@ -1,69 +0,0 @@ -value); - self::assertSame('$foo', $node->raw); - } - - #[Test] - public function toStringReturnsRaw(): void - { - $node = new VariableLiteralNode('$bar'); - - self::assertSame('$bar', (string) $node); - } - - #[Test] - public function parseWithoutDollarSignAddsDollar(): void - { - $node = VariableLiteralNode::parse('myVar'); - - self::assertSame('myVar', $node->value); - self::assertSame('$myVar', $node->raw); - } - - #[Test] - public function parseWithDollarSignKeepsValue(): void - { - $node = VariableLiteralNode::parse('$myVar'); - - self::assertSame('myVar', $node->value); - self::assertSame('$myVar', $node->raw); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $node = new VariableLiteralNode('$x'); - - self::assertSame(0, $node->offset); - } - - #[Test] - public function constructorThrowsWhenStringTooShort(): void - { - $this->expectException(\InvalidArgumentException::class); - new VariableLiteralNode('$'); - } - - #[Test] - public function constructorThrowsWhenMissingDollarSign(): void - { - $this->expectException(\InvalidArgumentException::class); - new VariableLiteralNode('foo'); - } -} diff --git a/libs/types/tests/LogicalTypeNodeTest.php b/libs/types/tests/LogicalTypeNodeTest.php new file mode 100644 index 0000000..9577ccf --- /dev/null +++ b/libs/types/tests/LogicalTypeNodeTest.php @@ -0,0 +1,285 @@ +}> + */ + public static function provideLogicalTypes(): iterable + { + yield UnionTypeNode::class => [UnionTypeNode::class]; + yield IntersectionTypeNode::class => [IntersectionTypeNode::class]; + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function twoStatementsAreEnough(string $class): void + { + $node = new $class([$this->type('A'), $this->type('B')]); + + self::assertCount(2, $node); + self::assertCount(2, $node->statements); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function anArbitraryNumberOfStatementsIsStored(string $class): void + { + $node = new $class([ + $this->type('A'), + $this->type('B'), + $this->type('C'), + $this->type('D'), + ]); + + self::assertCount(4, $node); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function statementsOrderIsPreserved(string $class): void + { + $a = $this->type('A'); + $b = $this->type('B'); + $c = $this->type('C'); + + $node = new $class([$a, $b, $c]); + + self::assertSame([$a, $b, $c], $node->statements); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function statementsAreAcceptedFromAnyTraversable(string $class): void + { + $a = $this->type('A'); + $b = $this->type('B'); + + $node = new $class(new \ArrayIterator([$a, $b])); + + self::assertSame([$a, $b], $node->statements); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function statementsAreAcceptedFromAGenerator(string $class): void + { + $a = $this->type('A'); + $b = $this->type('B'); + + $node = new $class((static function () use ($a, $b): \Generator { + yield $a; + yield $b; + })()); + + self::assertSame([$a, $b], $node->statements); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function statementsAreReindexedIntoAList(string $class): void + { + $a = $this->type('A'); + $b = $this->type('B'); + + $node = new $class([7 => $a, 42 => $b]); + + self::assertSame([$a, $b], $node->statements); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function aSingleStatementIsRejected(string $class): void + { + $this->expectException(\LogicException::class); + + new $class([$this->type('A')]); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function noStatementsAreRejected(string $class): void + { + $this->expectException(\LogicException::class); + + new $class([]); + } + + /** + * A statement built out of a single statement of the same kind is the very + * same statement: the nested one is flattened into it. + */ + #[Test] + #[DataProvider('provideLogicalTypes')] + public function aSingleNestedStatementIsUnwrapped(string $class): void + { + $a = $this->type('A'); + $b = $this->type('B'); + + $node = new $class([new $class([$a, $b])]); + + self::assertSame([$a, $b], $node->statements); + } + + /** + * A nested statement is flattened before the number of statements is + * checked, so a single type wrapped into a statement is still too little. + */ + #[Test] + #[DataProvider('provideLogicalTypes')] + public function aStatementFlattenedIntoASingleTypeIsRejected(string $class): void + { + $node = new $class([$this->type('A'), $this->type('B')]); + $node->statements = [$this->type('C')]; + + $this->expectException(\LogicException::class); + + new $class([$node]); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function nestedStatementsOfTheSameTypeAreFlattened(string $class): void + { + $a = $this->type('A'); + $b = $this->type('B'); + $c = $this->type('C'); + + $node = new $class([new $class([$a, $b]), $c]); + + self::assertSame([$a, $b, $c], $node->statements); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function deeplyNestedStatementsAreFlattened(string $class): void + { + $a = $this->type('A'); + $b = $this->type('B'); + $c = $this->type('C'); + $d = $this->type('D'); + + $node = new $class([new $class([new $class([$a, $b]), $c]), $d]); + + self::assertSame([$a, $b, $c, $d], $node->statements); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function nestedStatementsAreFlattenedInAnyPosition(string $class): void + { + $a = $this->type('A'); + $b = $this->type('B'); + $c = $this->type('C'); + + $node = new $class([$a, new $class([$b, $c])]); + + self::assertSame([$a, $b, $c], $node->statements); + } + + #[Test] + public function unionDoesNotFlattenIntersection(): void + { + $intersection = new IntersectionTypeNode([$this->type('A'), $this->type('B')]); + + $node = new UnionTypeNode([$intersection, $this->type('C')]); + + self::assertCount(2, $node); + self::assertSame($intersection, $node->statements[0]); + } + + #[Test] + public function intersectionDoesNotFlattenUnion(): void + { + $union = new UnionTypeNode([$this->type('A'), $this->type('B')]); + + $node = new IntersectionTypeNode([$union, $this->type('C')]); + + self::assertCount(2, $node); + self::assertSame($union, $node->statements[0]); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function iteratorYieldsStatements(string $class): void + { + $a = $this->type('A'); + $b = $this->type('B'); + + $node = new $class([$a, $b]); + + self::assertSame([$a, $b], \iterator_to_array($node->getIterator())); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function serializationRoundtripPreservesStatementsAndOffset(string $class): void + { + $node = new $class([$this->type('A'), $this->type('B')], 13); + + /** @var LogicalTypeNode $restored */ + $restored = \unserialize(\serialize($node)); + + self::assertInstanceOf($class, $restored); + self::assertSame(13, $restored->offset); + self::assertCount(2, $restored); + self::assertSame('A', $restored->statements[0]->name->toString()); + self::assertSame('B', $restored->statements[1]->name->toString()); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function theOffsetIsPassedThroughTheConstructor(string $class): void + { + $node = new $class([$this->type('A'), $this->type('B')], 42); + + self::assertSame(42, $node->offset); + self::assertSame(42, $node->getOffset()); + } + + #[Test] + #[DataProvider('provideLogicalTypes')] + public function defaultOffsetIsZero(string $class): void + { + $node = new $class([$this->type('A'), $this->type('B')]); + + self::assertSame(0, $node->offset); + } + + /** + * Building a statement out of an already built one must not cost more the + * longer it gets: the flattening is linear in the number of statements. + */ + #[Test] + #[DataProvider('provideLogicalTypes')] + public function flatteningIsLinear(string $class): void + { + $statements = []; + + for ($i = 0; $i < 1000; ++$i) { + $statements[] = $this->type('T' . $i); + } + + $node = new $class($statements); + + self::assertCount(1000, $node); + self::assertSame($statements, $node->statements); + } +} diff --git a/libs/types/tests/MaskNodeTest.php b/libs/types/tests/MaskNodeTest.php new file mode 100644 index 0000000..4a2b22e --- /dev/null +++ b/libs/types/tests/MaskNodeTest.php @@ -0,0 +1,95 @@ + $items each of which is either a literal + * segment or the "*" of a wildcard + */ + private static function mask(array $items): MaskNode + { + $parts = []; + + foreach ($items as $item) { + $parts[] = $item === '*' ? new WildcardNode() : new Identifier($item); + } + + return new MaskNode($parts); + } + + /** + * @return iterable, non-empty-string, list}> + */ + public static function provideMasks(): iterable + { + yield 'a wildcard alone' => [['*'], '*', []]; + yield 'a trailing wildcard' => [['BAR_', '*'], 'BAR_*', ['BAR_']]; + yield 'a leading wildcard' => [['*', '_BAR'], '*_BAR', ['_BAR']]; + yield 'a wildcard in between' => [['A', '*', 'B'], 'A*B', ['A', 'B']]; + yield 'several wildcards' => [ + ['BAR', '*', 'BAZ', '*', 'SOME'], + 'BAR*BAZ*SOME', + ['BAR', 'BAZ', 'SOME'], + ]; + yield 'wildcards all around' => [['*', 'A', '*'], '*A*', ['A']]; + } + + /** + * @param list $items + * @param non-empty-string $expected + * @param list $segments + */ + #[Test] + #[DataProvider('provideMasks')] + public function aMaskIsWrittenTheWayItIsRead(array $items, string $expected, array $segments): void + { + self::assertSame($expected, self::mask($items)->toString()); + self::assertSame($expected, (string) self::mask($items)); + } + + /** + * @param list $items + * @param non-empty-string $expected + * @param list $segments + */ + #[Test] + #[DataProvider('provideMasks')] + public function segmentsAreTheLiteralPartsAlone(array $items, string $expected, array $segments): void + { + $mask = self::mask($items); + + self::assertSame($segments, $mask->getSegmentsAsStrings()); + + $identifiers = []; + + foreach ($mask->getSegments() as $identifier) { + $identifiers[] = $identifier->value; + } + + self::assertSame($segments, $identifiers); + } + + #[Test] + public function everyPartOfAMaskIsKeptInTheOrderItIsWrittenIn(): void + { + $mask = self::mask(['A', '*', 'B']); + + self::assertCount(3, $mask); + self::assertInstanceOf(Identifier::class, $mask->items[0]); + self::assertInstanceOf(WildcardNode::class, $mask->items[1]); + self::assertInstanceOf(Identifier::class, $mask->items[2]); + } +} diff --git a/libs/types/tests/NameTest.php b/libs/types/tests/NameTest.php index e03974e..cfa5659 100644 --- a/libs/types/tests/NameTest.php +++ b/libs/types/tests/NameTest.php @@ -54,7 +54,7 @@ public function firstPropertyReturnsFirstSegment(): void { $name = new Name([$this->id('Foo'), $this->id('Bar')]); - self::assertSame('Foo', $name->first->value); + self::assertSame('Foo', $name->getFirstPart()->value); } #[Test] @@ -62,7 +62,7 @@ public function lastPropertyReturnsLastSegment(): void { $name = new Name([$this->id('Foo'), $this->id('Bar')]); - self::assertSame('Bar', $name->last->value); + self::assertSame('Bar', $name->getLastPart()->value); } #[Test] @@ -70,7 +70,7 @@ public function isSimpleIsTrueForSingleSegment(): void { $name = new Name([$this->id('Foo')]); - self::assertTrue($name->isSimple); + self::assertTrue($name->isSimple()); } #[Test] @@ -78,7 +78,7 @@ public function isSimpleIsFalseForMultipleSegments(): void { $name = new Name([$this->id('Foo'), $this->id('Bar')]); - self::assertFalse($name->isSimple); + self::assertFalse($name->isSimple()); } #[Test] @@ -86,7 +86,7 @@ public function isSpecialIsTrueForSpecialSingleSegment(): void { $name = new Name([$this->id('self')]); - self::assertTrue($name->isSpecial); + self::assertTrue($name->isSpecial()); } #[Test] @@ -94,7 +94,7 @@ public function isSpecialIsFalseForMultiSegmentNameStartingWithSpecial(): void { $name = new Name([$this->id('self'), $this->id('Foo')]); - self::assertFalse($name->isSpecial); + self::assertFalse($name->isSpecial()); } #[Test] @@ -102,7 +102,7 @@ public function isBuiltinIsTrueForBuiltinSingleSegment(): void { $name = new Name([$this->id('int')]); - self::assertTrue($name->isBuiltin); + self::assertTrue($name->isBuiltin()); } #[Test] @@ -110,7 +110,7 @@ public function isBuiltinIsFalseForMultiSegmentName(): void { $name = new Name([$this->id('int'), $this->id('Foo')]); - self::assertFalse($name->isBuiltin); + self::assertFalse($name->isBuiltin()); } #[Test] @@ -140,14 +140,6 @@ public function createFromStringDetectsFullyQualified(): void self::assertSame('\Foo\Bar', $name->toString()); } - #[Test] - public function createFromStringSegments(): void - { - $name = Name::createFromStringSegments(['Foo', 'Bar']); - - self::assertSame('Foo\Bar', $name->toString()); - } - #[Test] public function sliceReturnsSubName(): void { @@ -222,7 +214,7 @@ public function toStringArrayReturnsSegmentStrings(): void { $name = Name::createFromString('A\B\C'); - self::assertSame(['A', 'B', 'C'], $name->toStringArray()); + self::assertSame(['A', 'B', 'C'], $name->toArrayStrings()); } #[Test] @@ -230,7 +222,7 @@ public function toLowerStringArrayReturnsLowercasedSegments(): void { $name = Name::createFromString('Foo\Bar'); - self::assertSame(['foo', 'bar'], $name->toLowercaseStringArray()); + self::assertSame(['foo', 'bar'], $name->toArrayLowercaseStrings()); } #[Test] @@ -286,17 +278,443 @@ public function serializeAndUnserializeRoundtrip(): void $restored = \unserialize(\serialize($name)); self::assertInstanceOf(Name::class, $restored); - self::assertSame(['Foo', 'Bar'], $restored->toStringArray()); + self::assertSame(['Foo', 'Bar'], $restored->toArrayStrings()); self::assertSame(10, $restored->offset); } #[Test] public function constructorThrowsOnEmptySegmentsArray(): void { - self::skipWhenAssertsAreDisabled(); - $this->expectException(\InvalidArgumentException::class); new Name([]); } + + #[Test] + public function fullyQualifiedDefaultValueConstantIsFalse(): void + { + self::assertFalse(Name::IS_FULLY_QUALIFIED_DEFAULT_VALUE); + } + + #[Test] + public function toUnqualifiedLowerStringLowercasesNameWithoutLeadingDelimiter(): void + { + $name = Name::createFromString('\Vendor\Package\SomeClass'); + + self::assertSame('vendor\package\someclass', $name->toUnqualifiedLowerString()); + } + + #[Test] + public function toFullQualifiedLowerStringKeepsLeadingDelimiter(): void + { + $name = Name::createFromString('\Vendor\Package\SomeClass'); + + self::assertSame('\vendor\package\someclass', $name->toFullQualifiedLowerString()); + } + + #[Test] + public function toFullQualifiedStringIsIndependentOfTheQualificationFlag(): void + { + $name = Name::createFromString('Vendor\SomeClass'); + + self::assertFalse($name->isFullyQualified); + self::assertSame('\Vendor\SomeClass', $name->toFullQualifiedString()); + self::assertSame('\vendor\someclass', $name->toFullQualifiedLowerString()); + } + + #[Test] + public function toStringDependsOnTheQualificationFlag(): void + { + $unqualified = new Name([$this->id('Foo')], false); + $qualified = new Name([$this->id('Foo')], true); + + self::assertSame('Foo', $unqualified->toString()); + self::assertSame('\Foo', $qualified->toString()); + } + + #[Test] + public function createFromStringIgnoresRepeatedDelimiters(): void + { + $name = Name::createFromString('Foo\\\\Bar'); + + self::assertSame(['Foo', 'Bar'], $name->toArrayStrings()); + } + + #[Test] + public function createFromStringIgnoresTrailingDelimiter(): void + { + $name = Name::createFromString('Foo\Bar\\'); + + self::assertSame(['Foo', 'Bar'], $name->toArrayStrings()); + self::assertFalse($name->isFullyQualified); + } + + #[Test] + public function createFromStringAcceptsStringableObject(): void + { + $stringable = new class implements \Stringable { + public function __toString(): string + { + return 'Foo\Bar'; + } + }; + + self::assertSame(['Foo', 'Bar'], Name::createFromString($stringable)->toArrayStrings()); + } + + #[Test] + public function createFromStringThrowsOnEmptyString(): void + { + $this->expectException(\InvalidArgumentException::class); + + Name::createFromString(''); + } + + #[Test] + public function createFromStringThrowsOnDelimiterOnlyString(): void + { + $this->expectException(\InvalidArgumentException::class); + + Name::createFromString('\\'); + } + + #[Test] + public function sliceKeepsQualificationFlag(): void + { + $name = Name::createFromString('\Foo\Bar\Baz'); + + $sliced = $name->slice(1); + + self::assertSame(['Bar', 'Baz'], $sliced->toArrayStrings()); + self::assertTrue($sliced->isFullyQualified); + } + + #[Test] + public function sliceDoesNotModifyTheOriginalName(): void + { + $name = Name::createFromString('Foo\Bar\Baz'); + + $name->slice(1); + + self::assertSame(['Foo', 'Bar', 'Baz'], $name->toArrayStrings()); + } + + #[Test] + public function withAddedKeepsQualificationFlagOfTheReceiver(): void + { + $name = Name::createFromString('\Some\Any'); + + $result = $name->withAdded(Name::createFromString('Test\Class')); + + self::assertSame(['Some', 'Any', 'Test', 'Class'], $result->toArrayStrings()); + self::assertTrue($result->isFullyQualified); + } + + #[Test] + public function withAddedDoesNotModifyArguments(): void + { + $name = Name::createFromString('Some\Any'); + $added = Name::createFromString('Test'); + + $name->withAdded($added); + + self::assertSame(['Some', 'Any'], $name->toArrayStrings()); + self::assertSame(['Test'], $added->toArrayStrings()); + } + + #[Test] + public function mergeWithReplacesTheAliasSegment(): void + { + $name = Name::createFromString('TypeLang\Parser\Exception'); + + $result = $name->mergeWith(Name::createFromString('Error\SemanticException')); + + self::assertSame('TypeLang\Parser\Exception\SemanticException', $result->toString()); + } + + #[Test] + public function mergeWithSimpleNameReturnsTheReceiverSegments(): void + { + $name = Name::createFromString('TypeLang\Parser\Node'); + + $result = $name->mergeWith(Name::createFromString('Node')); + + self::assertSame('TypeLang\Parser\Node', $result->toString()); + } + + #[Test] + public function toFullQualifiedKeepsSegments(): void + { + $name = Name::createFromString('Foo\Bar'); + + $result = $name->toFullQualified(); + + self::assertTrue($result->isFullyQualified); + self::assertSame(['Foo', 'Bar'], $result->toArrayStrings()); + self::assertFalse($name->isFullyQualified, 'The original name must not be modified'); + } + + #[Test] + public function toUnqualifiedKeepsSegments(): void + { + $name = Name::createFromString('\Foo\Bar'); + + $result = $name->toUnqualified(); + + self::assertFalse($result->isFullyQualified); + self::assertSame(['Foo', 'Bar'], $result->toArrayStrings()); + self::assertTrue($name->isFullyQualified, 'The original name must not be modified'); + } + + #[Test] + public function toUnqualifiedOfUnqualifiedNameReturnsEqualName(): void + { + $name = Name::createFromString('Foo\Bar'); + + $result = $name->toUnqualified(); + + self::assertFalse($result->isFullyQualified); + self::assertSame('Foo\Bar', $result->toString()); + } + + #[Test] + public function serializationRoundtripPreservesQualificationFlag(): void + { + $name = Name::createFromString('\Foo\Bar'); + + /** @var Name $restored */ + $restored = \unserialize(\serialize($name)); + + self::assertTrue($restored->isFullyQualified); + self::assertSame('\Foo\Bar', $restored->toString()); + } + + #[Test] + public function serializePayloadContainsSegmentsOffsetAndFqnFlag(): void + { + $name = Name::createFromString('Foo'); + $name->offset = 3; + + self::assertSame([$name->parts, 3, false], $name->__serialize()); + } + + #[Test] + public function unserializeThrowsWhenSegmentsAreMissing(): void + { + $name = Name::createFromString('Foo'); + + $this->expectException(\UnexpectedValueException::class); + + $name->__unserialize([]); + } + + #[Test] + public function offsetMethodIsAnAliasOfProperty(): void + { + $name = Name::createFromString('Foo'); + $name->offset = 9; + + self::assertSame(9, $name->offset); + } + + #[Test] + public function stringCastIsTheSameAsToStringMethod(): void + { + $name = Name::createFromString('\Foo\Bar'); + + self::assertSame($name->toString(), (string) $name); + } + + #[Test] + public function getFirstPartReturnsTheFirstSegment(): void + { + $name = Name::createFromString('Foo\Bar\Baz'); + + self::assertSame($name->first, $name->getFirstPart()); + self::assertSame('Foo', $name->getFirstPart()->value); + } + + #[Test] + public function getFirstPartAsStringReturnsTheFirstSegmentValue(): void + { + $name = Name::createFromString('Foo\Bar'); + + self::assertSame('Foo', $name->getFirstPartAsString()); + } + + #[Test] + public function getFirstPartAsLowerStringLowercasesTheFirstSegment(): void + { + $name = Name::createFromString('FooBar\Baz'); + + self::assertSame('foobar', $name->getFirstPartAsLowerString()); + } + + #[Test] + public function getLastPartReturnsTheLastSegment(): void + { + $name = Name::createFromString('Foo\Bar\Baz'); + + self::assertSame($name->last, $name->getLastPart()); + self::assertSame('Baz', $name->getLastPart()->value); + } + + #[Test] + public function getLastPartAsStringReturnsTheLastSegmentValue(): void + { + $name = Name::createFromString('Foo\Bar'); + + self::assertSame('Bar', $name->getLastPartAsString()); + } + + #[Test] + public function getLastPartAsLowerStringLowercasesTheLastSegment(): void + { + $name = Name::createFromString('Foo\BarBaz'); + + self::assertSame('barbaz', $name->getLastPartAsLowerString()); + } + + #[Test] + public function partsOfASimpleNameAreTheSameSegment(): void + { + $name = Name::createFromString('Foo'); + + self::assertSame($name->first, $name->last); + } + + #[Test] + public function toArrayReturnsTheSegments(): void + { + $name = Name::createFromString('Foo\Bar'); + + self::assertSame($name->parts, $name->toArray()); + self::assertContainsOnlyInstancesOf(Identifier::class, $name->toArray()); + } + + #[Test] + public function isFullQualifiedIsAnAliasOfTheProperty(): void + { + $qualified = Name::createFromString('\Foo\Bar'); + $unqualified = Name::createFromString('Foo\Bar'); + + self::assertTrue($qualified->isFullQualified()); + self::assertFalse($unqualified->isFullQualified()); + } + + #[Test] + public function getPartsIsAnAliasOfTheProperty(): void + { + $name = Name::createFromString('Foo\Bar'); + + self::assertSame($name->parts, $name->getParts()); + } + + #[Test] + public function getPartsAsStringIsAnAliasOfToArrayStrings(): void + { + $name = Name::createFromString('Foo\Bar'); + + self::assertSame(['Foo', 'Bar'], $name->getPartsAsString()); + self::assertSame($name->toArrayStrings(), $name->getPartsAsString()); + } + + #[Test] + public function serializationRoundtripRestoresBoundaryParts(): void + { + $name = Name::createFromString('Foo\Bar\Baz'); + + /** @var Name $restored */ + $restored = \unserialize(\serialize($name)); + + self::assertSame('Foo', $restored->first->value); + self::assertSame('Baz', $restored->last->value); + self::assertSame('Foo', $restored->getFirstPart()->value); + self::assertSame('Baz', $restored->getLastPart()->value); + } + + #[Test] + public function unserializedNameIsFullyUsable(): void + { + $name = Name::createFromString('self'); + + /** @var Name $restored */ + $restored = \unserialize(\serialize($name)); + + self::assertTrue($restored->isSimple()); + self::assertTrue($restored->isSpecial()); + self::assertFalse($restored->isBuiltin()); + self::assertSame('self', $restored->getFirstPartAsString()); + } + + #[Test] + public function unserializeThrowsOnEmptyParts(): void + { + $this->expectException(\Throwable::class); + + \unserialize(self::payload([], 0, false)); + } + + #[Test] + public function unserializeThrowsOnNonIdentifierParts(): void + { + $this->expectException(\Throwable::class); + + \unserialize(self::payload(['Foo'], 0, false)); + } + + #[Test] + public function unserializeThrowsOnMissingParts(): void + { + $this->expectException(\UnexpectedValueException::class); + + \unserialize(self::payload()); + } + + /** + * Builds a serialized {@see Name} payload from the given + * {@see Name::__serialize()} data. + * + * @return non-empty-string + */ + private static function payload(mixed ...$data): string + { + $body = \serialize($data); + + return \vsprintf('O:%d:"%s":%d:%s', [ + \strlen(Name::class), + Name::class, + \count($data), + \substr($body, (int) \strpos($body, '{')), + ]); + } + + #[Test] + public function constructorThrowsOnNonIdentifierParts(): void + { + $this->expectException(\TypeError::class); + + /** @phpstan-ignore-next-line */ + new Name(['Foo']); + } + + #[Test] + public function sliceThrowsWhenTheResultIsEmpty(): void + { + $name = Name::createFromString('Foo\Bar'); + + $this->expectException(\InvalidArgumentException::class); + + $name->slice(5); + } + + #[Test] + public function sliceOfZeroLengthThrows(): void + { + $name = Name::createFromString('Foo\Bar'); + + $this->expectException(\InvalidArgumentException::class); + + $name->slice(0, 0); + } } diff --git a/libs/types/tests/NamedTypeNodeTest.php b/libs/types/tests/NamedTypeNodeTest.php index 30931a0..c3cc454 100644 --- a/libs/types/tests/NamedTypeNodeTest.php +++ b/libs/types/tests/NamedTypeNodeTest.php @@ -9,6 +9,7 @@ use TypeLang\Type\NamedTypeNode; use TypeLang\Type\Shape\FieldsListNode; use TypeLang\Type\Template\TemplateArgumentListNode; +use TypeLang\Type\Template\TemplateArgumentNode; final class NamedTypeNodeTest extends TestCase { @@ -40,7 +41,9 @@ public function fieldsDefaultToNull(): void #[Test] public function constructorStoresTemplateArguments(): void { - $args = new TemplateArgumentListNode(); + $args = new TemplateArgumentListNode([ + new TemplateArgumentNode(new NamedTypeNode(Name::createFromString('int'))), + ]); $node = new NamedTypeNode(Name::createFromString('array'), $args); self::assertSame($args, $node->arguments); diff --git a/libs/types/tests/NodeListTest.php b/libs/types/tests/NodeListTest.php new file mode 100644 index 0000000..57e7b34 --- /dev/null +++ b/libs/types/tests/NodeListTest.php @@ -0,0 +1,282 @@ + $items + * @return NodeList + */ + private function list(array $items = []): NodeList + { + /** @var NodeList */ + return new class ($items) extends NodeList {}; + } + + private function node(string $name = 'Example'): Identifier + { + return new Identifier($name); + } + + #[Test] + public function emptyByDefault(): void + { + $list = $this->list(); + + self::assertCount(0, $list); + self::assertSame([], $list->items); + } + + #[Test] + public function constructorAcceptsTraversable(): void + { + $generator = (function (): \Generator { + yield $this->node('A'); + yield $this->node('B'); + })(); + + $list = $this->list(); + $list = new class ($generator) extends NodeList {}; + + self::assertCount(2, $list); + self::assertSame('A', (string) $list->items[0]); + self::assertSame('B', (string) $list->items[1]); + } + + #[Test] + public function constructorReindexesNonListArrays(): void + { + $a = $this->node('A'); + $b = $this->node('B'); + + $list = new class ([7 => $a, 42 => $b]) extends NodeList {}; + + self::assertSame([$a, $b], $list->items); + } + + #[Test] + public function firstAndLastAreNullWhenEmpty(): void + { + $list = $this->list(); + + self::assertNull($list->first()); + self::assertNull($list->last()); + self::assertNull($list->first()); + self::assertNull($list->last()); + } + + #[Test] + public function firstAndLastReturnBoundaryNodes(): void + { + $a = $this->node('A'); + $b = $this->node('B'); + $c = $this->node('C'); + $list = $this->list([$a, $b, $c]); + + self::assertSame($a, $list->first()); + self::assertSame($c, $list->last()); + } + + #[Test] + public function firstAndLastAreTheSameNodeInSingleItemList(): void + { + $a = $this->node('A'); + $list = $this->list([$a]); + + self::assertSame($a, $list->first()); + self::assertSame($a, $list->last()); + } + + #[Test] + public function virtualPropertiesReflectListMutations(): void + { + $a = $this->node('A'); + $b = $this->node('B'); + $list = $this->list([$a]); + + $list->items[] = $b; + + self::assertSame($b, $list->last(), 'The $last property must be recalculated on each access'); + } + + #[Test] + public function findIndexReturnsPositionOfNode(): void + { + $a = $this->node('A'); + $b = $this->node('B'); + $list = $this->list([$a, $b]); + + self::assertSame(0, $list->findIndex($a)); + self::assertSame(1, $list->findIndex($b)); + } + + #[Test] + public function findIndexReturnsNullForUnknownNode(): void + { + $list = $this->list([$this->node('A')]); + + self::assertNull($list->findIndex($this->node('B'))); + } + + #[Test] + public function findIndexComparesByIdentityNotByValue(): void + { + $list = $this->list([$this->node('A')]); + + self::assertNull($list->findIndex($this->node('A'))); + } + + #[Test] + public function offsetExistsReflectsListContent(): void + { + $list = $this->list([$this->node('A')]); + + self::assertTrue($list->offsetExists(0)); + self::assertFalse($list->offsetExists(1)); + self::assertTrue(isset($list[0])); + self::assertFalse(isset($list[1])); + } + + #[Test] + public function offsetGetReturnsNodeOrNull(): void + { + $a = $this->node('A'); + $list = $this->list([$a]); + + self::assertSame($a, $list[0]); + self::assertNull($list[1]); + } + + #[Test] + public function offsetSetReplacesExistingNode(): void + { + $a = $this->node('A'); + $b = $this->node('B'); + $list = $this->list([$a]); + + $list[0] = $b; + + self::assertCount(1, $list); + self::assertSame($b, $list[0]); + } + + #[Test] + public function offsetSetKeepsListIndexedSequentially(): void + { + $list = $this->list([$this->node('A')]); + + $list[5] = $this->node('B'); + + self::assertSame([0, 1], \array_keys($list->items)); + } + + #[Test] + public function offsetSetWithoutIndexAppendsNode(): void + { + $a = $this->node('A'); + $b = $this->node('B'); + $list = $this->list([$a]); + + $list[] = $b; + + self::assertCount(2, $list); + self::assertSame($b, $list->last()); + } + + #[Test] + public function offsetUnsetRemovesNodeAndReindexes(): void + { + $a = $this->node('A'); + $b = $this->node('B'); + $c = $this->node('C'); + $list = $this->list([$a, $b, $c]); + + unset($list[1]); + + self::assertCount(2, $list); + self::assertSame([$a, $c], $list->items); + } + + #[Test] + public function offsetUnsetOfUnknownIndexKeepsListUnchanged(): void + { + $a = $this->node('A'); + $list = $this->list([$a]); + + unset($list[42]); + + self::assertSame([$a], $list->items); + } + + #[Test] + public function iteratorYieldsAllNodesInOrder(): void + { + $a = $this->node('A'); + $b = $this->node('B'); + $list = $this->list([$a, $b]); + + self::assertSame([$a, $b], \iterator_to_array($list->getIterator())); + } + + #[Test] + public function iteratorOfEmptyListYieldsNothing(): void + { + self::assertSame([], \iterator_to_array($this->list()->getIterator())); + } + + #[Test] + public function countReflectsNumberOfNodes(): void + { + self::assertCount(0, $this->list()); + self::assertCount(1, $this->list([$this->node('A')])); + self::assertCount(2, $this->list([$this->node('A'), $this->node('B')])); + } + + #[Test] + public function defaultOffsetIsZero(): void + { + self::assertSame(0, $this->list()->offset); + } + + #[Test] + public function offsetGetIsAnAliasOfTheArrayAccess(): void + { + $a = $this->node('A'); + $list = $this->list([$a]); + + self::assertSame($a, $list->offsetGet(0)); + self::assertNull($list->offsetGet(1)); + } + + #[Test] + public function offsetSetIsAnAliasOfTheArrayAccess(): void + { + $a = $this->node('A'); + $b = $this->node('B'); + $list = $this->list([$a]); + + $list->offsetSet(0, $b); + + self::assertSame([$b], $list->items); + } + + #[Test] + public function offsetUnsetIsAnAliasOfTheArrayAccess(): void + { + $a = $this->node('A'); + $b = $this->node('B'); + $list = $this->list([$a, $b]); + + $list->offsetUnset(0); + + self::assertSame([$b], $list->items); + } +} diff --git a/libs/types/tests/NodeTest.php b/libs/types/tests/NodeTest.php new file mode 100644 index 0000000..362b9ae --- /dev/null +++ b/libs/types/tests/NodeTest.php @@ -0,0 +1,375 @@ + + */ + public static function provideNodes(): iterable + { + yield CallableTypeNode::class => [new CallableTypeNode(Name::createFromString('callable'))]; + yield ClassConstMaskNode::class => [new ClassConstMaskNode(Name::createFromString('Example'))]; + yield ClassConstNode::class => [ + new ClassConstNode(Name::createFromString('Example'), new Identifier('CONST')), + ]; + yield ConstMaskNode::class => [new ConstMaskNode(new MaskNode([new Identifier('CONST'), new WildcardNode()]))]; + yield Identifier::class => [new Identifier('Example')]; + yield IntersectionTypeNode::class => [new IntersectionTypeNode([self::type('A'), self::type('B')])]; + yield MaskNode::class => [new MaskNode([new WildcardNode()])]; + yield Name::class => [Name::createFromString('Example')]; + yield NamedTypeNode::class => [self::type()]; + yield NullableTypeNode::class => [new NullableTypeNode(self::type())]; + yield TernaryExpressionNode::class => [ + new TernaryExpressionNode( + new EqualConditionNode(self::type('A'), self::type('B')), + self::type('C'), + self::type('D'), + ), + ]; + yield TypeOffsetAccessNode::class => [new TypeOffsetAccessNode(self::type(), self::type('Offset'))]; + yield TypesListNode::class => [new TypesListNode(self::type())]; + yield UnionTypeNode::class => [new UnionTypeNode([self::type('A'), self::type('B')])]; + yield WildcardNode::class => [new WildcardNode()]; + + yield CallableParameterListNode::class => [new CallableParameterListNode()]; + yield CallableParameterNode::class => [new CallableParameterNode(self::type())]; + + yield EqualConditionNode::class => [new EqualConditionNode(self::type('A'), self::type('B'))]; + yield NotEqualConditionNode::class => [new NotEqualConditionNode(self::type('A'), self::type('B'))]; + + yield BoolLiteralNode::class => [new BoolLiteralNode(true)]; + yield FloatLiteralNode::class => [new FloatLiteralNode(0.1)]; + yield IntLiteralNode::class => [new IntLiteralNode(42)]; + yield NullLiteralNode::class => [new NullLiteralNode()]; + yield StringLiteralNode::class => [new StringLiteralNode('example')]; + yield ThisNode::class => [new ThisNode()]; + yield VariableNode::class => [new VariableNode(new Identifier('example'))]; + + yield ComplexFieldNode::class => [ + new ComplexFieldNode( + new ClassConstNode(Name::createFromString('Example'), new Identifier('CONST')), + self::type(), + ), + ]; + yield FieldsListNode::class => [new FieldsListNode()]; + yield ImplicitFieldNode::class => [new ImplicitFieldNode(self::type())]; + yield NamedFieldNode::class => [new NamedFieldNode(new Identifier('key'), self::type())]; + yield ScalarFieldNode::class => [new ScalarFieldNode(new IntLiteralNode(0), self::type())]; + + yield TemplateArgumentListNode::class => [ + new TemplateArgumentListNode([new TemplateArgumentNode(self::type())]), + ]; + yield TemplateArgumentNode::class => [new TemplateArgumentNode(self::type())]; + yield TemplateBoundEdgeNode::class => [new TemplateBoundEdgeNode(new Identifier('of'), self::type())]; + yield TemplateParameterListNode::class => [new TemplateParameterListNode()]; + yield TemplateParameterNode::class => [new TemplateParameterNode(new Identifier('T'))]; + } + + #[Test] + #[DataProvider('provideNodes')] + public function everyNodeImplementsNodeInterface(Node $node): void + { + self::assertInstanceOf(NodeInterface::class, $node); + } + + #[Test] + #[DataProvider('provideNodes')] + public function everyNodeHasZeroOffsetByDefault(Node $node): void + { + self::assertSame(0, $node->offset); + self::assertSame(0, $node->offset); + } + + #[Test] + #[DataProvider('provideNodes')] + public function everyNodeOffsetIsWritableAndReflectedByMethod(Node $node): void + { + $node->offset = 42; + + self::assertSame(42, $node->offset); + self::assertSame(42, $node->offset, 'The offset() method must be an alias of the $offset property'); + } + + /** + * Guards the data provider above from getting out of sync with the package: + * every concrete node class must be listed there. + */ + #[Test] + public function providerCoversEveryConcreteNodeClass(): void + { + $covered = []; + + foreach (self::provideNodes() as [$node]) { + $covered[] = $node::class; + } + + $expected = []; + $directory = \dirname(__DIR__) . '/src'; + $files = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS), + ); + + foreach ($files as $file) { + if ($file->getExtension() !== 'php') { + continue; + } + + $relative = \substr($file->getPathname(), \strlen($directory) + 1, -4); + $class = 'TypeLang\\Type\\' . \str_replace(['/', '\\'], '\\', $relative); + + if (!\class_exists($class)) { + continue; + } + + $reflection = new \ReflectionClass($class); + + if ($reflection->isAbstract() || !$reflection->isSubclassOf(Node::class)) { + continue; + } + + $expected[] = $class; + } + + \sort($expected); + \sort($covered); + + self::assertSame($expected, $covered); + } + + #[Test] + public function abstractNodeOffsetDefaultsToZero(): void + { + $node = new class extends Node {}; + + self::assertSame(0, $node->offset); + } + + #[Test] + public function typeNodeIsANode(): void + { + $node = new class extends TypeNode {}; + + self::assertInstanceOf(Node::class, $node); + self::assertInstanceOf(NodeInterface::class, $node); + } + + #[Test] + public function wrappingTypeNodeStoresWrappedType(): void + { + $inner = self::type(); + $node = new TypesListNode($inner); + + self::assertSame($inner, $node->type); + } + + #[Test] + public function wrappingTypeNodeTypeIsMutable(): void + { + $node = new TypesListNode(self::type('A')); + $node->type = self::type('B'); + + self::assertSame('B', $node->type->name->toString()); + } + + #[Test] + #[DataProvider('provideNodes')] + public function everyNodeOffsetIsReadableByMethod(Node $node): void + { + self::assertSame(0, $node->getOffset()); + + $node->offset = 42; + + self::assertSame(42, $node->getOffset()); + } + + #[Test] + public function abstractNodeOffsetIsReadableByMethod(): void + { + $node = new class extends Node {}; + + self::assertSame(0, $node->getOffset()); + } + + /** + * Every node accepts the source code offset it has been read at as the + * last argument of its constructor. + * + * @return iterable + */ + public static function provideNodesWithOffset(): iterable + { + yield CallableTypeNode::class => [ + new CallableTypeNode(Name::createFromString('callable'), offset: 42), + ]; + yield ClassConstMaskNode::class => [ + new ClassConstMaskNode(Name::createFromString('Example'), offset: 42), + ]; + yield ClassConstNode::class => [ + new ClassConstNode(Name::createFromString('Example'), new Identifier('CONST'), 42), + ]; + yield ConstMaskNode::class => [ + new ConstMaskNode(new MaskNode([new Identifier('CONST'), new WildcardNode()]), offset: 42), + ]; + yield Identifier::class => [new Identifier('Example', 42)]; + yield MaskNode::class => [new MaskNode([new WildcardNode()], 42)]; + yield Name::class => [Name::createFromString('Example', 42)]; + yield NamedTypeNode::class => [new NamedTypeNode(Name::createFromString('Example'), offset: 42)]; + yield NullableTypeNode::class => [new NullableTypeNode(self::type(), 42)]; + yield TernaryExpressionNode::class => [ + new TernaryExpressionNode( + new EqualConditionNode(self::type('A'), self::type('B')), + self::type('C'), + self::type('D'), + 42, + ), + ]; + yield TypeOffsetAccessNode::class => [ + new TypeOffsetAccessNode(self::type(), self::type('Offset'), 42), + ]; + yield TypesListNode::class => [new TypesListNode(self::type(), 42)]; + yield WildcardNode::class => [new WildcardNode(42)]; + + yield CallableParameterListNode::class => [new CallableParameterListNode([], 42)]; + yield CallableParameterNode::class => [new CallableParameterNode(self::type(), offset: 42)]; + + yield EqualConditionNode::class => [new EqualConditionNode(self::type('A'), self::type('B'), 42)]; + yield NotEqualConditionNode::class => [ + new NotEqualConditionNode(self::type('A'), self::type('B'), 42), + ]; + + yield BoolLiteralNode::class => [new BoolLiteralNode(true, offset: 42)]; + yield FloatLiteralNode::class => [new FloatLiteralNode(0.1, offset: 42)]; + yield IntLiteralNode::class => [new IntLiteralNode(42, offset: 42)]; + yield NullLiteralNode::class => [new NullLiteralNode(offset: 42)]; + yield StringLiteralNode::class => [new StringLiteralNode('example', offset: 42)]; + yield ThisNode::class => [new ThisNode(42)]; + yield VariableNode::class => [new VariableNode(new Identifier('example'), 42)]; + + yield ComplexFieldNode::class => [ + new ComplexFieldNode( + new ClassConstNode(Name::createFromString('Example'), new Identifier('CONST')), + self::type(), + offset: 42, + ), + ]; + yield FieldsListNode::class => [new FieldsListNode(offset: 42)]; + yield ImplicitFieldNode::class => [new ImplicitFieldNode(self::type(), offset: 42)]; + yield NamedFieldNode::class => [ + new NamedFieldNode(new Identifier('key'), self::type(), offset: 42), + ]; + yield ScalarFieldNode::class => [ + new ScalarFieldNode(new IntLiteralNode(0), self::type(), offset: 42), + ]; + + yield TemplateArgumentListNode::class => [ + new TemplateArgumentListNode([new TemplateArgumentNode(self::type())], 42), + ]; + yield TemplateArgumentNode::class => [new TemplateArgumentNode(self::type(), offset: 42)]; + yield TemplateBoundEdgeNode::class => [ + new TemplateBoundEdgeNode(new Identifier('of'), self::type(), 42), + ]; + yield TemplateParameterListNode::class => [new TemplateParameterListNode([], 42)]; + yield TemplateParameterNode::class => [ + new TemplateParameterNode(new Identifier('T'), offset: 42), + ]; + + yield UnionTypeNode::class => [new UnionTypeNode([self::type('A'), self::type('B')], 42)]; + yield IntersectionTypeNode::class => [ + new IntersectionTypeNode([self::type('A'), self::type('B')], 42), + ]; + } + + #[Test] + #[DataProvider('provideNodesWithOffset')] + public function everyNodeConstructorPassesTheOffsetThrough(Node $node): void + { + self::assertSame(42, $node->offset); + self::assertSame(42, $node->getOffset()); + } + + /** + * Guards the provider above from getting out of sync with the package: + * every node takes the offset as the last argument of its constructor and + * must be listed there. + */ + #[Test] + public function everyNodeIsCoveredByTheOffsetProvider(): void + { + $expected = $actual = []; + + foreach (self::provideNodes() as [$node]) { + $parameters = (new \ReflectionObject($node)) + ->getConstructor()?->getParameters() ?? []; + + $last = \end($parameters); + + self::assertNotFalse($last, $node::class . ' must take an offset'); + self::assertSame('offset', $last->getName(), $node::class . ' must take an offset last'); + + $expected[] = $node::class; + } + + foreach (self::provideNodesWithOffset() as [$node]) { + $actual[] = $node::class; + } + + \sort($expected); + \sort($actual); + + self::assertSame($expected, $actual); + } +} diff --git a/libs/types/tests/Shape/ClassConstFieldNodeTest.php b/libs/types/tests/Shape/ClassConstFieldNodeTest.php deleted file mode 100644 index 0c59191..0000000 --- a/libs/types/tests/Shape/ClassConstFieldNodeTest.php +++ /dev/null @@ -1,84 +0,0 @@ -makeKey('MyClass', 'STATUS'); - $type = new NamedTypeNode(Name::createFromString('int')); - $node = new ClassConstFieldNode($key, $type); - - self::assertSame($key, $node->key); - self::assertSame($type, $node->type); - } - - #[Test] - public function indexFormatsAsClassDoubleColonConst(): void - { - $key = $this->makeKey('MyEnum', 'ACTIVE'); - $node = new ClassConstFieldNode($key, new NamedTypeNode(Name::createFromString('string'))); - - self::assertSame('MyEnum::ACTIVE', $node->index); - } - - #[Test] - public function indexWithQualifiedClassName(): void - { - $key = $this->makeKey('Vendor\Package\Status', 'OK'); - $node = new ClassConstFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - - self::assertSame('Vendor\Package\Status::OK', $node->index); - } - - #[Test] - public function optionalDefaultsToFalse(): void - { - $node = new ClassConstFieldNode( - $this->makeKey('Foo', 'BAR'), - new NamedTypeNode(Name::createFromString('int')), - ); - - self::assertFalse($node->isOptional); - } - - #[Test] - public function constructorStoresOptionalFlag(): void - { - $node = new ClassConstFieldNode( - $this->makeKey('Foo', 'BAR'), - new NamedTypeNode(Name::createFromString('int')), - true, - ); - - self::assertTrue($node->isOptional); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $node = new ClassConstFieldNode( - $this->makeKey('A', 'B'), - new NamedTypeNode(Name::createFromString('int')), - ); - - self::assertSame(0, $node->offset); - } -} diff --git a/libs/types/tests/Shape/ClassConstMaskFieldNodeTest.php b/libs/types/tests/Shape/ClassConstMaskFieldNodeTest.php deleted file mode 100644 index dfd1e71..0000000 --- a/libs/types/tests/Shape/ClassConstMaskFieldNodeTest.php +++ /dev/null @@ -1,79 +0,0 @@ -key); - self::assertSame($type, $node->type); - } - - #[Test] - public function indexWithConstantSuffix(): void - { - $key = new ClassConstMaskNode(Name::createFromString('MyEnum'), new Identifier('STATUS')); - $node = new ClassConstMaskFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - - self::assertSame('MyEnum::STATUS*', $node->index); - } - - #[Test] - public function indexWithNullConstant(): void - { - $key = new ClassConstMaskNode(Name::createFromString('MyEnum')); - $node = new ClassConstMaskFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - - self::assertSame('MyEnum::*', $node->index); - } - - #[Test] - public function optionalDefaultsToFalse(): void - { - $node = new ClassConstMaskFieldNode( - new ClassConstMaskNode(Name::createFromString('Foo')), - new NamedTypeNode(Name::createFromString('int')), - ); - - self::assertFalse($node->isOptional); - } - - #[Test] - public function constructorStoresOptionalFlag(): void - { - $node = new ClassConstMaskFieldNode( - new ClassConstMaskNode(Name::createFromString('Foo')), - new NamedTypeNode(Name::createFromString('int')), - true, - ); - - self::assertTrue($node->isOptional); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $node = new ClassConstMaskFieldNode( - new ClassConstMaskNode(Name::createFromString('A')), - new NamedTypeNode(Name::createFromString('int')), - ); - - self::assertSame(0, $node->offset); - } -} diff --git a/libs/types/tests/Shape/ConstMaskFieldNodeTest.php b/libs/types/tests/Shape/ConstMaskFieldNodeTest.php deleted file mode 100644 index c973946..0000000 --- a/libs/types/tests/Shape/ConstMaskFieldNodeTest.php +++ /dev/null @@ -1,78 +0,0 @@ -key); - self::assertSame($type, $node->type); - } - - #[Test] - public function indexReturnsConstMaskNodeAsString(): void - { - $key = new ConstMaskNode(Name::createFromString('MY_CONST')); - $node = new ConstMaskFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - - self::assertSame('MY_CONST*', $node->index); - } - - #[Test] - public function indexWithQualifiedName(): void - { - $key = new ConstMaskNode(Name::createFromString('Vendor\Pkg\STATUS')); - $node = new ConstMaskFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - - self::assertSame('Vendor\Pkg\STATUS*', $node->index); - } - - #[Test] - public function optionalDefaultsToFalse(): void - { - $node = new ConstMaskFieldNode( - new ConstMaskNode(Name::createFromString('FOO')), - new NamedTypeNode(Name::createFromString('int')), - ); - - self::assertFalse($node->isOptional); - } - - #[Test] - public function constructorStoresOptionalFlag(): void - { - $node = new ConstMaskFieldNode( - new ConstMaskNode(Name::createFromString('FOO')), - new NamedTypeNode(Name::createFromString('int')), - true, - ); - - self::assertTrue($node->isOptional); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $node = new ConstMaskFieldNode( - new ConstMaskNode(Name::createFromString('X')), - new NamedTypeNode(Name::createFromString('int')), - ); - - self::assertSame(0, $node->offset); - } -} diff --git a/libs/types/tests/Shape/ExplicitFieldNodeTest.php b/libs/types/tests/Shape/ExplicitFieldNodeTest.php new file mode 100644 index 0000000..6d272ab --- /dev/null +++ b/libs/types/tests/Shape/ExplicitFieldNodeTest.php @@ -0,0 +1,198 @@ + + */ + public static function provideExplicitFields(): iterable + { + yield 'named' => [new NamedFieldNode(new Identifier('key'), self::type())]; + yield 'scalar' => [new ScalarFieldNode(new IntLiteralNode(42), self::type())]; + yield 'complex' => [ + new ComplexFieldNode( + new ClassConstNode(Name::createFromString('Vendor\Status'), new Identifier('OK')), + self::type(), + ), + ]; + } + + /** + * Only a key that comes down to a string of its own carries an index. + * + * @return iterable + */ + public static function provideIndexedFields(): iterable + { + yield 'named' => [ + new NamedFieldNode(new Identifier('key'), self::type()), + 'key', + ]; + + yield 'named by a keyword' => [ + new NamedFieldNode(new Identifier('true'), self::type()), + 'true', + ]; + + yield 'string' => [ + new ScalarFieldNode(new StringLiteralNode('some key'), self::type()), + 'some key', + ]; + + yield 'string carrying a sequence' => [ + new ScalarFieldNode(new StringLiteralNode("a\nb"), self::type()), + "a\nb", + ]; + + yield 'number' => [ + new ScalarFieldNode(new IntLiteralNode(42), self::type()), + '42', + ]; + + yield 'zero' => [ + new ScalarFieldNode(new IntLiteralNode(0), self::type()), + '0', + ]; + + yield 'negative number' => [ + new ScalarFieldNode(new IntLiteralNode(-1), self::type()), + '-1', + ]; + } + + + #[Test] + #[DataProvider('provideIndexedFields')] + public function indexMethodReturnsTheKeyAsAString(SimpleFieldNodeInterface $field, string $index): void + { + self::assertSame($index, $field->getIndex()); + } + + /** + * A key that has to be read to be understood offers no index: Whether two + * of them name the same constant is not written down. + * + * @return iterable + */ + public static function provideComplexFields(): iterable + { + yield 'class const' => [ + new ComplexFieldNode( + new ClassConstNode(Name::createFromString('Vendor\Status'), new Identifier('OK')), + self::type(), + ), + ]; + + yield 'class const mask' => [ + new ComplexFieldNode( + new ClassConstMaskNode( + Name::createFromString('Vendor\Status'), + new MaskNode([new Identifier('IS_'), new WildcardNode()]), + ), + self::type(), + ), + ]; + + yield 'const mask' => [ + new ComplexFieldNode( + new ConstMaskNode( + new MaskNode([new Identifier('STATUS_'), new WildcardNode()]), + Name::createFromString('Vendor'), + ), + self::type(), + ), + ]; + } + + #[Test] + #[DataProvider('provideComplexFields')] + public function complexFieldCarriesNoIndex(ComplexFieldNode $field): void + { + self::assertNotInstanceOf(SimpleFieldNodeInterface::class, $field); + self::assertInstanceOf(TypeNode::class, $field->key); + } + + #[Test] + public function indexIsRecalculatedAfterKeyMutation(): void + { + $field = new NamedFieldNode(new Identifier('key'), self::type()); + + self::assertSame('key', $field->getIndex()); + + $field->key = new Identifier('other'); + + self::assertSame('other', $field->getIndex(), 'The index must be derived from the current key'); + } + + #[Test] + #[DataProvider('provideExplicitFields')] + public function explicitFieldIsRequiredByDefault(ExplicitFieldNode $field): void + { + self::assertFalse($field->isOptional); + } + + #[Test] + #[DataProvider('provideExplicitFields')] + public function explicitFieldIsAFieldNode(ExplicitFieldNode $field): void + { + self::assertInstanceOf(FieldNode::class, $field); + self::assertInstanceOf(TypeNode::class, $field->type); + } + + #[Test] + public function optionalFieldStoresTheFlag(): void + { + $field = new NamedFieldNode(new Identifier('key'), self::type(), true); + + self::assertTrue($field->isOptional); + } + + #[Test] + public function implicitFieldHasNoKey(): void + { + $field = new ImplicitFieldNode(self::type()); + + self::assertInstanceOf(FieldNode::class, $field); + self::assertNotInstanceOf(ExplicitFieldNode::class, $field); + } + + #[Test] + public function fieldTypeIsMutable(): void + { + $field = new NamedFieldNode(new Identifier('key'), self::type('A')); + $field->type = self::type('B'); + + self::assertSame('B', $field->type->name->toString()); + } +} diff --git a/libs/types/tests/Shape/FieldsListNodeTest.php b/libs/types/tests/Shape/FieldsListNodeTest.php index 4f04c54..111c50e 100644 --- a/libs/types/tests/Shape/FieldsListNodeTest.php +++ b/libs/types/tests/Shape/FieldsListNodeTest.php @@ -24,8 +24,8 @@ public function emptyListByDefault(): void $list = new FieldsListNode(); self::assertCount(0, $list); - self::assertNull($list->first); - self::assertNull($list->last); + self::assertNull($list->first()); + self::assertNull($list->last()); } #[Test] @@ -33,7 +33,7 @@ public function sealedByDefault(): void { $list = new FieldsListNode(); - self::assertTrue($list->sealed); + self::assertTrue($list->isSealed); } #[Test] @@ -41,7 +41,7 @@ public function constructorAcceptsUnsealedFlag(): void { $list = new FieldsListNode([], false); - self::assertFalse($list->sealed); + self::assertFalse($list->isSealed); } #[Test] @@ -52,24 +52,8 @@ public function constructorAcceptsFields(): void $list = new FieldsListNode([$a, $b]); self::assertCount(2, $list); - self::assertSame($a, $list->first); - self::assertSame($b, $list->last); - } - - #[Test] - public function toStringReturnsSealedWhenSealed(): void - { - $list = new FieldsListNode([], true); - - self::assertSame('sealed', (string) $list); - } - - #[Test] - public function toStringReturnsUnsealedWhenNotSealed(): void - { - $list = new FieldsListNode([], false); - - self::assertSame('unsealed', (string) $list); + self::assertSame($a, $list->first()); + self::assertSame($b, $list->last()); } #[Test] diff --git a/libs/types/tests/Shape/ImplicitFieldNodeTest.php b/libs/types/tests/Shape/ImplicitFieldNodeTest.php index 51bfd85..d1e70df 100644 --- a/libs/types/tests/Shape/ImplicitFieldNodeTest.php +++ b/libs/types/tests/Shape/ImplicitFieldNodeTest.php @@ -40,33 +40,6 @@ public function constructorStoresOptionalFlag(): void self::assertTrue($node->isOptional); } - #[Test] - public function toStringReturnsRequiredWhenNotOptional(): void - { - $node = new ImplicitFieldNode(new NamedTypeNode(Name::createFromString('int'))); - - self::assertSame('required', (string) $node); - } - - #[Test] - public function toStringReturnsOptionalWhenOptional(): void - { - $node = new ImplicitFieldNode( - new NamedTypeNode(Name::createFromString('int')), - true, - ); - - self::assertSame('optional', (string) $node); - } - - #[Test] - public function attributesDefaultToNull(): void - { - $node = new ImplicitFieldNode(new NamedTypeNode(Name::createFromString('int'))); - - self::assertNull($node->attributes); - } - #[Test] public function defaultOffsetIsZero(): void { diff --git a/libs/types/tests/Shape/NamedFieldNodeTest.php b/libs/types/tests/Shape/NamedFieldNodeTest.php index 75d59ae..fb6d6b8 100644 --- a/libs/types/tests/Shape/NamedFieldNodeTest.php +++ b/libs/types/tests/Shape/NamedFieldNodeTest.php @@ -30,7 +30,7 @@ public function indexReturnsKeyValue(): void $key = new Identifier('myField'); $node = new NamedFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - self::assertSame('myField', $node->index); + self::assertSame('myField', $node->getIndex()); } #[Test] @@ -56,29 +56,6 @@ public function constructorStoresOptionalFlag(): void self::assertTrue($node->isOptional); } - #[Test] - public function toStringReturnsRequiredWhenNotOptional(): void - { - $node = new NamedFieldNode( - new Identifier('field'), - new NamedTypeNode(Name::createFromString('string')), - ); - - self::assertSame('required', (string) $node); - } - - #[Test] - public function toStringReturnsOptionalWhenOptional(): void - { - $node = new NamedFieldNode( - new Identifier('field'), - new NamedTypeNode(Name::createFromString('string')), - true, - ); - - self::assertSame('optional', (string) $node); - } - #[Test] public function defaultOffsetIsZero(): void { diff --git a/libs/types/tests/Shape/NumericFieldNodeTest.php b/libs/types/tests/Shape/NumericFieldNodeTest.php deleted file mode 100644 index f01f6aa..0000000 --- a/libs/types/tests/Shape/NumericFieldNodeTest.php +++ /dev/null @@ -1,78 +0,0 @@ -key); - self::assertSame($type, $node->type); - } - - #[Test] - public function indexReturnsStringifiedKeyValue(): void - { - $key = IntLiteralNode::parse('42'); - $node = new NumericFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - - self::assertSame('42', $node->index); - } - - #[Test] - public function indexForZeroKey(): void - { - $key = IntLiteralNode::parse('0'); - $node = new NumericFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - - self::assertSame('0', $node->index); - } - - #[Test] - public function optionalDefaultsToFalse(): void - { - $node = new NumericFieldNode( - IntLiteralNode::parse('1'), - new NamedTypeNode(Name::createFromString('string')), - ); - - self::assertFalse($node->isOptional); - } - - #[Test] - public function constructorStoresOptionalFlag(): void - { - $node = new NumericFieldNode( - IntLiteralNode::parse('1'), - new NamedTypeNode(Name::createFromString('string')), - true, - ); - - self::assertTrue($node->isOptional); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $node = new NumericFieldNode( - IntLiteralNode::parse('0'), - new NamedTypeNode(Name::createFromString('int')), - ); - - self::assertSame(0, $node->offset); - } -} diff --git a/libs/types/tests/Shape/StringNamedFieldNodeTest.php b/libs/types/tests/Shape/StringNamedFieldNodeTest.php deleted file mode 100644 index 3fe395b..0000000 --- a/libs/types/tests/Shape/StringNamedFieldNodeTest.php +++ /dev/null @@ -1,69 +0,0 @@ -key); - self::assertSame($type, $node->type); - } - - #[Test] - public function indexReturnsKeyValue(): void - { - $key = new StringLiteralNode('field-name'); - $node = new StringNamedFieldNode($key, new NamedTypeNode(Name::createFromString('int'))); - - self::assertSame('field-name', $node->index); - } - - #[Test] - public function optionalDefaultsToFalse(): void - { - $node = new StringNamedFieldNode( - new StringLiteralNode('key'), - new NamedTypeNode(Name::createFromString('string')), - ); - - self::assertFalse($node->isOptional); - } - - #[Test] - public function constructorStoresOptionalFlag(): void - { - $node = new StringNamedFieldNode( - new StringLiteralNode('key'), - new NamedTypeNode(Name::createFromString('string')), - true, - ); - - self::assertTrue($node->isOptional); - } - - #[Test] - public function defaultOffsetIsZero(): void - { - $node = new StringNamedFieldNode( - new StringLiteralNode('k'), - new NamedTypeNode(Name::createFromString('int')), - ); - - self::assertSame(0, $node->offset); - } -} diff --git a/libs/types/tests/Template/TemplateArgumentNodeTest.php b/libs/types/tests/Template/TemplateArgumentNodeTest.php index 8b6265e..62560ea 100644 --- a/libs/types/tests/Template/TemplateArgumentNodeTest.php +++ b/libs/types/tests/Template/TemplateArgumentNodeTest.php @@ -30,14 +30,6 @@ public function hintDefaultsToNull(): void self::assertNull($node->hint); } - #[Test] - public function attributesDefaultToNull(): void - { - $node = new TemplateArgumentNode(new NamedTypeNode(Name::createFromString('int'))); - - self::assertNull($node->attributes); - } - #[Test] public function constructorAcceptsIdentifierHint(): void { diff --git a/libs/types/tests/Template/TemplateArgumentsListNodeTest.php b/libs/types/tests/Template/TemplateArgumentsListNodeTest.php index dc541c6..5139b19 100644 --- a/libs/types/tests/Template/TemplateArgumentsListNodeTest.php +++ b/libs/types/tests/Template/TemplateArgumentsListNodeTest.php @@ -19,13 +19,22 @@ private function makeArg(string $type): TemplateArgumentNode } #[Test] - public function emptyListByDefault(): void + public function emptyListIsNotAllowed(): void { - $list = new TemplateArgumentListNode(); + $this->expectException(\InvalidArgumentException::class); - self::assertCount(0, $list); - self::assertNull($list->first); - self::assertNull($list->last); + new TemplateArgumentListNode([]); + } + + #[Test] + public function singleArgumentIsBothTheFirstAndTheLast(): void + { + $argument = $this->makeArg('string'); + $list = new TemplateArgumentListNode([$argument]); + + self::assertCount(1, $list); + self::assertSame($argument, $list->first()); + self::assertSame($argument, $list->last()); } #[Test] @@ -36,8 +45,8 @@ public function constructorAcceptsArguments(): void $list = new TemplateArgumentListNode([$a, $b]); self::assertCount(2, $list); - self::assertSame($a, $list->first); - self::assertSame($b, $list->last); + self::assertSame($a, $list->first()); + self::assertSame($b, $list->last()); } #[Test] @@ -103,7 +112,7 @@ public function iteratorYieldsArguments(): void #[Test] public function defaultOffsetIsZero(): void { - $list = new TemplateArgumentListNode(); + $list = new TemplateArgumentListNode([$this->makeArg('int')]); self::assertSame(0, $list->offset); } diff --git a/libs/types/tests/UnionTypeNodeTest.php b/libs/types/tests/UnionTypeNodeTest.php index 0c4d7ed..7f0f88a 100644 --- a/libs/types/tests/UnionTypeNodeTest.php +++ b/libs/types/tests/UnionTypeNodeTest.php @@ -22,7 +22,7 @@ public function constructorWithTwoTypes(): void { $a = $this->named('int'); $b = $this->named('string'); - $node = new UnionTypeNode($a, $b); + $node = new UnionTypeNode([$a, $b]); self::assertCount(2, $node); self::assertSame([$a, $b], $node->statements); @@ -34,7 +34,7 @@ public function constructorWithThreeTypes(): void $a = $this->named('int'); $b = $this->named('string'); $c = $this->named('null'); - $node = new UnionTypeNode($a, $b, $c); + $node = new UnionTypeNode([$a, $b, $c]); self::assertCount(3, $node); } @@ -46,8 +46,8 @@ public function nestedUnionIsFlattened(): void $b = $this->named('string'); $c = $this->named('null'); - $inner = new UnionTypeNode($a, $b); - $outer = new UnionTypeNode($inner, $c); + $inner = new UnionTypeNode([$a, $b]); + $outer = new UnionTypeNode([$inner, $c]); self::assertCount(3, $outer); self::assertSame([$a, $b, $c], $outer->statements); @@ -58,7 +58,7 @@ public function iteratorYieldsStatements(): void { $a = $this->named('int'); $b = $this->named('string'); - $node = new UnionTypeNode($a, $b); + $node = new UnionTypeNode([$a, $b]); self::assertSame([$a, $b], \iterator_to_array($node)); } @@ -66,7 +66,7 @@ public function iteratorYieldsStatements(): void #[Test] public function countReturnsNumberOfStatements(): void { - $node = new UnionTypeNode($this->named('int'), $this->named('string'), $this->named('bool')); + $node = new UnionTypeNode([$this->named('int'), $this->named('string'), $this->named('bool')]); self::assertSame(3, $node->count()); } @@ -78,8 +78,8 @@ public function intersectionTypeNodeIsNotFlattenedInsideUnion(): void $b = $this->named('B'); $c = $this->named('C'); - $inner = new IntersectionTypeNode($a, $b); - $outer = new UnionTypeNode($inner, $c); + $inner = new IntersectionTypeNode([$a, $b]); + $outer = new UnionTypeNode([$inner, $c]); self::assertCount(2, $outer); self::assertSame($inner, $outer->statements[0]); @@ -90,7 +90,7 @@ public function serializeAndUnserializeRoundtrip(): void { $a = $this->named('int'); $b = $this->named('string'); - $node = new UnionTypeNode($a, $b); + $node = new UnionTypeNode([$a, $b]); $node->offset = 5; /** @var UnionTypeNode $restored */ diff --git a/libs/types/tests/VariableNodeTest.php b/libs/types/tests/VariableNodeTest.php new file mode 100644 index 0000000..9940fc8 --- /dev/null +++ b/libs/types/tests/VariableNodeTest.php @@ -0,0 +1,67 @@ +name->toString()); + } + + #[Test] + public function singleCharacterNameIsAllowed(): void + { + $node = new VariableNode(new Identifier('a')); + + self::assertSame('a', $node->name->toString()); + } + + #[Test] + public function defaultOffsetIsZero(): void + { + $node = new VariableNode(new Identifier('x')); + + self::assertSame(0, $node->offset); + } + + /** + * A variable names a place a value is kept in, so it is no type of + * its own. + */ + #[Test] + public function variableIsNotAType(): void + { + $node = new VariableNode(new Identifier('foo')); + + self::assertInstanceOf(Node::class, $node); + self::assertNotInstanceOf(TypeNode::class, $node); + } + + /** + * The one variable that is a type of its own is the `$this`. + */ + #[Test] + public function thisIsAType(): void + { + self::assertInstanceOf(TypeNode::class, new ThisNode()); + } + + #[Test] + public function thisDefaultOffsetIsZero(): void + { + self::assertSame(0, (new ThisNode())->offset); + } +} diff --git a/libs/types/tests/WildcardNodeTest.php b/libs/types/tests/WildcardNodeTest.php new file mode 100644 index 0000000..d1b60a7 --- /dev/null +++ b/libs/types/tests/WildcardNodeTest.php @@ -0,0 +1,37 @@ +toString()); + self::assertSame('*', (string) $node); + self::assertSame('*', WildcardNode::CHAR); + } + + #[Test] + public function defaultOffsetIsZero(): void + { + self::assertSame(0, (new WildcardNode())->offset); + } +} diff --git a/phpbench.json b/phpbench.json index 4e6f73c..0890f63 100644 --- a/phpbench.json +++ b/phpbench.json @@ -1,14 +1,22 @@ { "$schema": "./vendor/phpbench/phpbench/phpbench.schema.json", "runner.bootstrap": "vendor/autoload.php", - "runner.path": "tests", + "runner.path": "bench", "runner.progress": "plain", "runner.file_pattern": "*Bench.php", "runner.php_config": { "opcache.enable": 1, + "zend.assertions": -1, "opcache.enable_cli": 1, + "opcache.jit": "tracing", "opcache.jit_buffer_size": "128M", - "xdebug.mode": "off" + "opcache.jit_hot_loop": 1, + "opcache.jit_hot_func": 1, + "opcache.jit_hot_return": 1, + "opcache.jit_hot_side_exit": 1, + "xdebug.mode": "off", + "pcov.enabled": 0, + "memory_limit": -1 }, "storage.xml_storage_path": "var", "storage.store_binary": true, @@ -20,34 +28,62 @@ "components": [ { "component": "section", - "tabbed": true, - "tab_labels": [ - "Time", - "Memory" + "title": "Results", + "partition": [ + "subject_name" ], "components": [ { - "component": "section", - "title": "Results", - "components": [ - { - "component": "table_aggregate", - "title": "{{ first(frame.suite_tag) }}", + "component": "table_aggregate", + "title": "{{ first(frame['subject_name']) }}", + "partition": [ + "benchmark_name", + "variant_name" + ], + "row": { + "benchmark": "first(partition['benchmark_name'])", + "set": "first(partition['variant_name'])", + "memory": "first(partition['result_mem_peak']) as memory", + "min": "min(partition['result_time_avg']) as time", + "max": "max(partition['result_time_avg']) as time", + "mode": "mode(partition['result_time_avg']) as time", + "rstdev": "rstdev(partition['result_time_avg'])" + } + } + ] + } + ] + }, + "compare": { + "extends": "overview", + "tabbed": false, + "components": [ + { + "component": "section", + "title": "Comparison", + "partition": [ + "subject_name" + ], + "components": [ + { + "component": "table_aggregate", + "title": "{{ first(frame['subject_name']) }}", + "partition": [ + "variant_name" + ], + "row": { + "set": "first(partition['variant_name'])", + "tools": { + "type": "expand", + "var": "tool", "partition": [ - "benchmark_name", - "subject_name", - "variant_name" + "coalesce(first(subject_groups), benchmark_name)" ], - "row": { - "benchmark": "first(partition['benchmark_name'])", - "memory": "first(partition['result_mem_peak']) as memory", - "min": "min(partition['result_time_avg']) as time", - "max": "max(partition['result_time_avg']) as time", - "mode": "mode(partition['result_time_avg']) as time", - "rstdev": "rstdev(partition['result_time_avg'])" + "cols": { + "{{ key }}": "(mode(tool['result_time_avg']) as time) ~ if(count(partition[contains(subject_groups, 'baseline')]['result_time_avg']) > 0, if(mode(partition[contains(subject_groups, 'baseline')]['result_time_avg']) > 0, ' (' ~ format('%.2fx', mode(tool['result_time_avg']) / mode(partition[contains(subject_groups, 'baseline')]['result_time_avg'])) ~ ')', ''), '')" } } - ] + } } ] } diff --git a/phpstan.neon b/phpstan.neon index 8a1867d..3a8b6e2 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -10,6 +10,10 @@ includes: - libs/types/phpstan.neon parameters: level: max + treatPhpDocTypesAsCertain: false + phpVersion: + min: 80100 + max: 80599 strictRules: allRules: true fileExtensions:
    Visibility Method or property visibility.
    AssertSubjectCodeReferenceWhat an assertion is written of: $value, + $this->items, $this->getItems() + or self::$instances.
    AuthorName string