diff --git a/.travis.yml b/.travis.yml index d0bfa28..4cfafc2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,16 +3,20 @@ language: php php: - 7.0 - 7.1 + - 7.2 + - 7.3 + - 7.4snapshot - nightly matrix: allow_failures: - php: nightly + - php: 7.4snapshot sudo: false before_script: - - phpenv config-rm xdebug.ini + - phpenv config-rm xdebug.ini || true - composer self-update - composer require satooshi/php-coveralls:~2.0.0 --no-update --dev - composer install --prefer-source diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ec6d6c0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM golang:1.10.1-alpine3.7 as builder + +WORKDIR /go/src/nubank/authorizer + +RUN apk --update add git openssh && \ + rm -rf /var/lib/apt/lists/* && \ + rm /var/cache/apk/* + +RUN go get -u github.com/golang/dep/cmd/dep + +COPY . . + +RUN dep ensure + +RUN go test -v ./... + +RUN go build -ldflags "-s -w" -o ./authorize + +FROM alpine:3.7 + +WORKDIR /app + +RUN apk add --no-cache ca-certificates + +COPY --from=builder /go/src/nubank/authorizer/authorize /usr/local/bin/ + +CMD authorize diff --git a/bin/yay b/bin/yay index fac326f..6d4698e 100755 --- a/bin/yay +++ b/bin/yay @@ -1,7 +1,7 @@ -#!/usr/bin/env php +#!/usr/bin/env php + yay --macros= + yay --macros= + yay -h | --help + yay --version + +Options: + -h --help Show this screen. + --version Show version. + --macros= PHP glob pattern for macros to be loaded. Ex: my/macros/*.yay [default: ''] + +Examples: + +``` +# Process file: +yay input.php > output.php + +# Process file, preload project macros: +> yay --macros="./project-macros/*.yay" input.php > output.php + +# Process stdin: +> cat input.php | yay > output.php + +# Process stdin, preload project macros: +> cat input.php | yay --macros="./project-macros/*.yay" > output.php +``` +DOC; + + $argv = Docopt::handle($doc, include __DIR__ . '/../meta.php'); + + $file = $argv[''] ?? 'php://stdin'; + + $source = file_get_contents($file); + + $engine = new Engine; gc_disable(); - $expansion = (new Engine)->expand($source, $file); + foreach(glob((string) $argv['--macros']) as $f) $engine->expand(file_get_contents($f), $f); + + $expansion = $engine->expand($source, $file); gc_enable(); file_put_contents('php://stdout', $expansion); } +catch (YayPreprocessorError $e) { + file_put_contents('php://stderr', $e . PHP_EOL); +} catch (Exception $e) { file_put_contents('php://stderr', $e . PHP_EOL); } diff --git a/composer.json b/composer.json index bbe9631..0f416ea 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,8 @@ "php": "7.*", "ext-mbstring": "*", "ext-tokenizer": "*", - "nikic/php-parser": "^2.1|^3.0|^4.0" + "nikic/php-parser": "^2.1|^3.0|^4.0", + "docopt/docopt": "^1.0" }, "autoload": { "files": [ diff --git a/meta.php b/meta.php new file mode 100644 index 0000000..4fd01d3 --- /dev/null +++ b/meta.php @@ -0,0 +1,3 @@ + '0.7' +]; diff --git a/src/Ast.php b/src/Ast.php index 3fe5ee6..41de9b5 100644 --- a/src/Ast.php +++ b/src/Ast.php @@ -2,14 +2,12 @@ namespace Yay; -use - InvalidArgumentException, - ArrayIterator, - TypeError -; - class Ast implements Result { + const + NULL_LABEL = '_' + ; + protected $label = '', $ast = [] @@ -21,12 +19,29 @@ class Ast implements Result { function __construct(string $label = '', $ast = []) { if ($ast instanceof self) - throw new InvalidArgumentException('Unmerged AST.'); + throw new YayPreprocessorError('Unmerged AST.'); $this->ast = $ast; $this->label = $label; } + function __set($path, $value) { + return $this->set($path, $value); + } + + function set($strPath, $value) { + $keys = preg_split('/\s+/', $strPath); + + if ([] === $keys) return; + + $current = &$this->ast; + foreach ($keys as $key) { + if (!is_array($current)) $current = []; + $current = &$current[$key]; + } + $current = $value; + } + function __get($path) { return $this->get($path); } @@ -68,7 +83,8 @@ function tokens() { array_walk_recursive( $exposed, - function($i) use(&$tokens){ + function($i, $key) use(&$tokens){ + if (0 === strpos((string) $key, self::NULL_LABEL)) return; if($i instanceof Token) $tokens[] = $i; elseif ($i instanceof self) $tokens = array_merge($tokens, $i->tokens()); } @@ -103,7 +119,7 @@ function array() { } function list() { - foreach (array_keys($this->array()) as $index) yield $index => $this->{"* {$index}"}; + foreach (array_keys($this->array()) as $i) if($i !== self::NULL_LABEL) yield $i => $this->{"* {$i}"}; } function flatten() : self { @@ -113,8 +129,7 @@ function flatten() : self { function append(self $ast) : self { if ('' !== $ast->label) { if (isset($this->ast[$ast->label])) - throw new InvalidArgumentException( - "Duplicated AST label '{$ast->label}'."); + throw new YayPreprocessorError("Duplicated AST label '{$ast->label}'."); $this->ast[$ast->label] = $ast->ast; } @@ -130,7 +145,7 @@ function push(self $ast) : self { } function isEmpty() : bool { - return null === $this->ast || 0 === \count($this->ast); + return null === $this->ast || [] === $this->ast; } function as(string $label = '') : Result { @@ -150,7 +165,7 @@ function withMeta(Map $meta) : Result { } function meta() : Map { - return $this->meta ?: Map::fromEmpty(); + return $this->meta ?: $this->meta = Map::fromEmpty(); } @@ -189,6 +204,6 @@ private function getIn(array $array, array $keys, $default = null) } private function failCasting(string $type) { - throw new YayException(sprintf("Ast cannot be casted to '%s'", $type)); + throw new YayPreprocessorError(sprintf("Ast cannot be casted to '%s'", $type)); } } diff --git a/src/Engine.php b/src/Engine.php index bf4fbcd..b44a580 100644 --- a/src/Engine.php +++ b/src/Engine.php @@ -110,8 +110,6 @@ function(Ast $node) :string { return (string) $node->{'* tag'}->token(); }, $macro = new Macro($tags, $pattern, $compilerPass, $expansion); $this->registerDirective($macro); - - if ($macro->tags()->contains('global')) $this->globalDirectives[] = $macro; }) ; @@ -160,6 +158,8 @@ function registerDirective(Directive $directive) { krsort($this->typeHitMap[$expected->type()]); } } + + if ($directive->tags()->contains('global')) $this->globalDirectives[] = $directive; } function blueContext() : BlueContext { @@ -175,28 +175,43 @@ function currentFileName() : string { } function expand(string $source, string $filename = '', int $gc = self::GC_ENGINE_ENABLED) : string { - $this->filename = $filename; foreach ($this->globalDirectives as $d) $this->registerDirective($d); $ts = TokenStream::{$filename && self::GC_ENGINE_ENABLED === $gc ? 'fromSource' : 'FromSourceWithoutOpenTag'}($source); - ($this->expander)($ts); - $expansion = (string) $ts; - - if (self::GC_ENGINE_ENABLED === $gc) { - // almost everything is local per file so state must be destroyed after expansion - // unless the flag ::GC_ENGINE_ENABLED forces a recycle during nested expansions - // global directives are allocated again later to give impression of persistence - // ::GC_ENGINE_DISABLED indicates the current pass is an internal Engine recursion - $this->cycle = new Cycle; - $this->literalHitMap= $this->typeHitMap = []; - $this->blueContext = new BlueContext; - } - - $this->filename = ''; + try { + ($this->expander)($ts); - return $expansion; + return (string) $ts; + } + catch(YayPreprocessorError $error) { + throw new class( + str_replace(' on line', sprintf(', in %s on line', $this->filename), $error->getMessage()), + $error->getCode(), + $error, + $this->filename ?: '-', + 1 + ) extends YayPreprocessorError { + function __construct($message = '', $code = 0, \Throwable $error = null, $file = '', $line = 0) { + parent::__construct($message, $code, $error); + $this->file = $file; + $this->line = $line; + } + }; + } + finally { + if (self::GC_ENGINE_ENABLED === $gc) { + // almost everything is local per file so state must be destroyed after expansion + // unless the flag ::GC_ENGINE_ENABLED forces a recycle during nested expansions + // global directives are allocated again later to give impression of persistence + // ::GC_ENGINE_DISABLED indicates the current pass is an internal Engine recursion + $this->cycle = new Cycle; + $this->literalHitMap= $this->typeHitMap = []; + $this->blueContext = new BlueContext; + } + $this->filename = ''; + } } } diff --git a/src/Error.php b/src/Error.php index 8955e2a..dd567e2 100644 --- a/src/Error.php +++ b/src/Error.php @@ -78,8 +78,4 @@ function message() : string { return implode(PHP_EOL, $messages); } - - function halt() { - throw new Halt($this->message()); - } } diff --git a/src/Expansion.php b/src/Expansion.php index aad5b3b..fb55863 100644 --- a/src/Expansion.php +++ b/src/Expansion.php @@ -206,15 +206,8 @@ private function mutate(TokenStream $ts, Ast $context, Engine $engine) : TokenSt consume($this->expanderExpansion())->onCommit(function(Ast $result) use ($states) { $cg = $states->current(); - $expander = $result->{'* expander'}; - if (\count($result->{'args'}) === 0) - $cg->this->fail(self::E_EMPTY_EXPANDER_SLICE, $expander->implode(), $expander->tokens()[0]->line()); + $mutation = $this->doExpanderCall($result, $cg); - $expansion = TokenStream::fromSlice($result->{'args'}); - $mutation = $cg->this->mutate($expansion, $cg->context, $cg->engine); - - $expander = $cg->this->compileCallable('\Yay\Dsl\Expanders\\', $expander, self::E_BAD_EXPANDER); - $mutation = $expander($mutation, $cg->engine); $cg->ts->inject($mutation); }) , @@ -230,19 +223,29 @@ private function mutate(TokenStream $ts, Ast $context, Engine $engine) : TokenSt $context = $context->unwrap(); + if (! is_array($context)) + $this->fail( + "Error unpacking a non unpackable Ast node on `$(%s%s... {` at line %d with context: %s\n\n%s", + $result->{'* label _name'}->token(), + $result->optional, + $result->{'* label _name'}->token()->line(), + json_encode([$context], self::PRETTY_PRINT), + sprintf("Hint: use a non ellipsis expansion as in `$(%s %s {`", $result->{'* label _name'}->token(), $result->optional) + ); + $delimiters = $result->{'delimiters'}; // normalize associative arrays if (array_values($context) !== $context) $context = [$context]; - foreach (array_reverse($context, true) as $i => $iterationContext) { + foreach (array_reverse($context, true) as $i => $scope) { if ($key = $result->{'key'}) { - $iterationContext[(string) $result->{'key'}] = new Token(T_LNUMBER, (string) $i); + $scope[(string) $result->{'key'}] = new Token(T_LNUMBER, (string) $i); } $expansion = TokenStream::fromSlice($result->{'expansion'}); $mutation = $cg->this->mutate( $expansion, - (new Ast('', $cg->context->unwrap() + $iterationContext)), + (new Ast('', $cg->context->unwrap() + (is_array($scope) ? $scope : [$scope]))), $cg->engine ); if ($i !== count($context)-1) foreach ($delimiters as $d) $mutation->push($d); @@ -297,14 +300,14 @@ private function mutate(TokenStream $ts, Ast $context, Engine $engine) : TokenSt } private function lookupAst(Ast $label, Ast $context, string $error) : Ast { - $symbol = $label->{'* name'}->token(); + $symbol = $label->{'* _name'}->token(); if (null === ($result = $context->get('* ' . $symbol))->unwrap()) { $this->fail( $error, - $label->{'complex'} ? $label->{'* complex_name'}->implode() : $symbol, + $label->{'_complex'} ? $label->{'* _complex_name'}->implode() : $symbol, $symbol->line(), json_encode( - $label->{'complex'} + $label->{'_complex'} ? array_values(array_filter($context->symbols(), 'is_string')) : $context->symbols() , @@ -317,11 +320,82 @@ private function lookupAst(Ast $label, Ast $context, string $error) : Ast { } private function lookupAstOptional(Ast $label, Ast $context) : Ast { - $result = $context->get('* ' . (string) $label->{'* name'}->token()); + $result = $context->get('* ' . (string) $label->{'* _name'}->token()); return $result; } + private function doExpanderCall(Ast $ast, $cg) : TokenStream { + $function = $cg->this->compileCallable('\Yay\Dsl\Expanders\\', $ast->{'* expander'}, self::E_BAD_EXPANDER); + $expander = new \ReflectionFunction($function); + $delayed = function($expander) { return $expander->invoke(); }; + if ($expander->getParameters()) { + if (($class = $expander->getParameters()[0]->getClass()) && Ast::class === $class->getName()) { + $delayed = function($expander, $ast, $cg) { return $this->doAstExpanderCall($expander, $ast, $cg); }; + } else { + $delayed = function($expander, $ast, $cg) { return $this->doTokenStreamExpanderCall($expander, $ast, $cg); }; + } + } + + $mutation = $delayed($expander, $ast, $cg); + + if (! ($mutation instanceof TokenStream || $mutation instanceof Ast)) $this->fail( + 'Expander call `%s(%s)` must return Ast or TokenStream, %s returned on line %d', + $expander->getName(), + implode( + ', ', + array_map(function($p){ return preg_replace('/Parameter #\d+ \[ |<.+> | \]/', '', $p); }, + $expander->getParameters()) + ), + gettype($mutation), + $ast->{'* expander'}->tokens()[0]->line() + ); + + if ($mutation instanceof Ast) $mutation = TokenStream::fromSlice($mutation->tokens()); + + return $mutation; + } + + private function doTokenStreamExpanderCall(\ReflectionFunction $expander, Ast $expanderAst, $cg): TokenStream { + $ts = TokenStream::fromSlice(array_slice($expanderAst->{'* args'}->tokens(), 1, -1)); + + if ($ts->isEmpty()) $this->fail( + 'TokenStream expander call without tokens `%s` as function %s(%s) on line %d', + $expanderAst->implode(), + $expander->getName(), + implode( + ', ', + array_map(function($p){ return preg_replace('/Parameter #\d+ \[ |<.+> | \]/', '', $p); }, + $expander->getParameters()) + ), + $expanderAst->{'* expander'}->tokens()[0]->line() + ); + + return $expander->invoke($cg->this->mutate($ts, $cg->context, $cg->engine), $cg->engine); + } + + private function doAstExpanderCall(\ReflectionFunction $expander, Ast $expanderAst, $cg) { + $arg = null; + if ($expanderAst->{'args leaf_arg'}) { + $arg = $this->lookupAst($expanderAst->{'* args leaf_arg label'}, $cg->context, self::E_UNDEFINED_EXPANSION); + } + else if($expanderAst->{'args expander_call'}) { + $arg = $this->doAstExpanderCall( + new \ReflectionFunction( + $cg->this->compileCallable( + '\Yay\Dsl\Expanders\\', + $expanderAst->{'* args expander_call expander'}, + self::E_BAD_EXPANDER + ) + ), + $expanderAst->{'* args expander_call'}, + $cg + ); + } + + return $expander->invoke($arg, $cg->engine); + } + private function conditionalLabelExpansion() : Parser { return sigil @@ -352,8 +426,37 @@ private function expanderExpansion() : Parser { ( ns()->as('expander') , - either(parentheses(), braces())->as('args') + either + ( + chain(token('('), optional($this->expanderAstExpansion()), token(')')) // recursion !!! + , + chain(token('('), $this->labelExpansion()->as('leaf_arg'), token(')')) + , + chain(token('('), optional(layer()->as('layer_arg')), token(')')) + , + chain(token('{'), optional(layer()->as('layer_arg')), token('}')) + ) + ->as('args') + ) + ->as('expander_call') + ; + } + + private function expanderAstExpansion() : Parser { + return + $expander = expander_sigil + ( + ns()->as('expander') + , + either + ( + chain(token('('), pointer($expander), token(')')) // recursion !!! + , + chain(token('('), $this->labelExpansion()->as('leaf_arg'), token(')')) + ) + ->as('args') ) + ->as('expander_call') ; } diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 03e6e27..7f6a657 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -75,7 +75,7 @@ private function rebuildParser() : Parser $arguments = pointer($arguments)->as('arguments_pointer'); // } - if (null === $this->stack[0]) { + if (null === $this->stack) { $this->addOperator(self::ARITY_UNARY|self::ASSOC_RIGHT, token(T_INCLUDE), token(T_INCLUDE_ONCE), token(T_REQUIRE), token(T_REQUIRE_ONCE)); $this->addOperator(self::ARITY_BINARY|self::ASSOC_LEFT, chain(token(T_LOGICAL_OR), optional(indentation()))); $this->addOperator(self::ARITY_BINARY|self::ASSOC_LEFT, chain(token(T_LOGICAL_XOR), optional(indentation()))); @@ -406,7 +406,7 @@ private function inferPositionalBehavior(int $flags) : int { foreach ([self::POS_PREFIX, self::POS_INFIX, self::POS_SUFFIX] as $p) if(($flags & $p) === $flags) return $p; - throw new \Exception('Could not infer operator positional behaviour.'); + throw new YayPreprocessorError('Could not infer operator positional behaviour.'); } /** @@ -414,7 +414,7 @@ private function inferPositionalBehavior(int $flags) : int */ private function validateBitTable(int $flags, string $message) { - if (! ($flags && !($flags & ($flags-1)))) throw new \Exception($message); + if (! ($flags && !($flags & ($flags-1)))) throw new YayPreprocessorError($message); } private function inferPrecedence() : int diff --git a/src/Halt.php b/src/Halt.php deleted file mode 100644 index e9804cf..0000000 --- a/src/Halt.php +++ /dev/null @@ -1,5 +0,0 @@ -compilerPass = $compilerPass; $this->expansion = $expansion; - $this->enableParserTracer = $this->tags->contains('enable_parser_tarcer'); + $this->enableParserTracer = $this->tags->contains('enable_parser_tracer'); $this->isTerminal = !$this->expansion->isRecursive(); } diff --git a/src/MacroMember.php b/src/MacroMember.php index 21553d8..8d96aef 100644 --- a/src/MacroMember.php +++ b/src/MacroMember.php @@ -13,7 +13,7 @@ abstract class MacroMember { ; protected function fail(string $error, ...$args) { - throw new YayParseError(sprintf($error, ...$args)); + throw new YayPreprocessorError(sprintf($error, ...$args)); } protected function compileCallable(string $namespace, Ast $type, string $error): callable { diff --git a/src/Parser.php b/src/Parser.php index 611559d..739edf1 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -2,10 +2,6 @@ namespace Yay; -use - InvalidArgumentException -; - use Yay\ParserTracer\{ParserTracer, NullParserTracer}; /** @@ -79,7 +75,7 @@ function parse(TokenStream $ts) /*: Result|null*/ self::$tracer->trace($index, 'error'); } } - catch(Halt $e) { + catch(YayPreprocessorError $e) { $ts->jump($index); self::$tracer->trace($index, 'error'); @@ -109,7 +105,7 @@ function as(string $label) : self { if ('' !== (string) $label) { if(false !== strpos($label, ' ')) - throw new InvalidArgumentException( + throw new YayPreprocessorError( "Parser label cannot contain spaces, '{$label}' given."); $this->label = $label; diff --git a/src/Pattern.php b/src/Pattern.php index 4a2ef28..73101ac 100644 --- a/src/Pattern.php +++ b/src/Pattern.php @@ -255,6 +255,44 @@ private function compileAlias(Ast $alias) : string { return $identifier; } + protected function compileArray(Ast $array) : array { + $compiled = []; + foreach ($array->{'* values'}->list() as $valueNode) { + $key = count($compiled); + $value = $valueNode->{'* value'}; + + if ($valueNode->{'key_value_pair'}) { + $value = $valueNode->{'* key_value_pair value'}; + $type = key($valueNode->{'* key_value_pair key'}->array()); + $key = $valueNode->{"* key_value_pair key {$type}"}; + switch ($type) { + case 'string': + $key = trim((string) $key->token(), '"\''); + break; + case 'int': + $key = (int) $key->token()->value(); + } + } + + $type = key($value->array()); + $value = $value->{"* {$type}"}; + switch ($type) { + case 'string': + $value = trim((string) $value->token(), '"\''); + break; + case 'int': + $value = (int) $value->token()->value(); + break; + case 'array': + $value = $this->compileArray($value); + } + + $compiled[$key] = $value; + } + + return $compiled; + } + protected function compileParser(Ast $ast) : Parser { $parser = $this->compileCallable('\Yay\\', $ast->{'* type'}, self::E_BAD_PARSER_NAME); $args = $ast->{'* args'}->isEmpty() ? [] : $this->compileParserArgs($ast->{'* args'}); @@ -291,6 +329,9 @@ protected function compileParserArgs(Ast $args) : array { case 'string': $compiled[] = trim((string) $arg->token(), '"\''); break; + case 'array': + $compiled[] = $this->compileArray($arg); + break; case 'function': // function(...){...} $compiled[] = new AnonymousFunction($arg); break; diff --git a/src/StreamWrapper.php b/src/StreamWrapper.php deleted file mode 100644 index 1d32fc3..0000000 --- a/src/StreamWrapper.php +++ /dev/null @@ -1,151 +0,0 @@ -isReadable()) return false; - - $opened_path = $path; - - $source = self::$engine->expand(file_get_contents($fileMeta->getRealPath()), $fileMeta->getRealPath()); - - $this->resource = fopen('php://memory', 'rb+'); - fwrite($this->resource, $source); - rewind($this->resource); - - return true; - } - - function stream_close() { - fclose($this->resource); - } - - function stream_read($length) : string { - $source = - ! feof($this->resource) - ? fread($this->resource, $length) - : '' - ; - - return $source; - } - - function stream_eof() : bool { return feof($this->resource); } - - function stream_stat() : array - { - $stat = fstat($this->resource); - if ($stat) { - $stat[self::STAT_MTIME_ASSOC_OFFSET]++; - $stat[self::STAT_MTIME_NUMERIC_OFFSET]++; - } - return $stat; - } - - function url_stat() { $this->notImplemented(__FUNCTION__); } - - function stream_write() { $this->notImplemented(__FUNCTION__); } - - function stream_truncate() { $this->notImplemented(__FUNCTION__); } - - function stream_metadata() { $this->notImplemented(__FUNCTION__); } - - function stream_tell() { $this->notImplemented(__FUNCTION__); } - - function stream_seek() { $this->notImplemented(__FUNCTION__); } - - function stream_flush() { $this->notImplemented(__FUNCTION__); } - - function stream_cast() { $this->notImplemented(__FUNCTION__); } - - function stream_lock() { $this->notImplemented(__FUNCTION__); } - - function stream_set_option() { $this->notImplemented(__FUNCTION__); } - - function unlink() { $this->notImplemented(__FUNCTION__); } - - function rename() { $this->notImplemented(__FUNCTION__); } - - function mkdir() { $this->notImplemented(__FUNCTION__); } - - function rmdir() { $this->notImplemented(__FUNCTION__); } - - function dir_opendir() { $this->notImplemented(__FUNCTION__); } - - function dir_readdir() { $this->notImplemented(__FUNCTION__); } - - function dir_rewinddir() { $this->notImplemented(__FUNCTION__); } - - function dir_closedir() { $this->notImplemented(__FUNCTION__); } - - private function notImplemented(string $from) { - throw new YayException(__CLASS__ . "->{$from} is not implemented."); - } -} diff --git a/src/TokenStream.php b/src/TokenStream.php index 3ecebc0..068b79f 100644 --- a/src/TokenStream.php +++ b/src/TokenStream.php @@ -2,10 +2,6 @@ namespace Yay; -use - InvalidArgumentException -; - class TokenStream { protected diff --git a/src/YayException.php b/src/YayException.php deleted file mode 100644 index f8d5360..0000000 --- a/src/YayException.php +++ /dev/null @@ -1,5 +0,0 @@ -current()) { $str = (string) $t; if (! preg_match('/^\w+$/', $str)) - throw new YayException( + throw new YayPreprocessorError( "Only valid identifiers are mergeable, '{$t->dump()}' given."); $buffer .= $str; diff --git a/src/parsers.php b/src/parsers.php index 7c556cf..bccaa06 100644 --- a/src/parsers.php +++ b/src/parsers.php @@ -2,10 +2,6 @@ namespace Yay; -use - InvalidArgumentException -; - function token($type, $value = null) : Parser { $token = $type instanceof Token ? $type : new Token($type, $value); @@ -68,7 +64,7 @@ function isFallible() : bool function rtoken(string $regexp) : Parser { if (false === preg_match($regexp, '')) - throw new InvalidArgumentException('Invalid regexp at ' . __FUNCTION__); + throw new YayPreprocessorError('Invalid regexp at ' . __FUNCTION__); return new class(__FUNCTION__, $regexp) extends Parser { @@ -267,7 +263,7 @@ function isFallible() : bool function repeat(Parser $parser) : Parser { if (! $parser->isFallible()) - throw new InvalidArgumentException( + throw new YayPreprocessorError( 'Infinite loop at ' . __FUNCTION__ . '('. $parser . '(*))'); return new class(__FUNCTION__, $parser) extends Parser @@ -534,7 +530,7 @@ function either(Parser ...$routes) : Parser foreach ($routes as $i => $route) if ($route !== $last && ! $route->isFallible()) { $parser = $routes[++$i]; - throw new InvalidArgumentException( + throw new YayPreprocessorError( "Dead {$parser}() parser at " . __FUNCTION__ . "(...[{$i}])"); } @@ -601,7 +597,7 @@ function optimize() : Parser foreach ($parser->expected()->all() as $prefixToken) $jumptable[$prefixToken->type()][] = $parser->optimize(); else - throw new \Exception("Cannot optimize {$this} parser stack at {$parser}"); + throw new YayPreprocessorError("Cannot optimize {$this} parser stack at {$parser}"); foreach ($jumptable as $prefix => $possibleRoutes) { if (count($possibleRoutes) > 1) $jumptable[$prefix] = either(...$possibleRoutes); @@ -719,7 +715,7 @@ function isFallible() : bool function optional(Parser $parser, $default = []) : Parser { if ($default instanceof Parser) - throw new InvalidArgumentException("optional() default value must not be "); + throw new YayPreprocessorError("optional() default value must not be "); return new class(__FUNCTION__, $parser, $default) extends Parser { @@ -751,7 +747,7 @@ protected function parser(TokenStream $ts, Parser $parser) : Ast { $result = $parser->withErrorLevel(Error::ENABLED)->parse($ts); - if ($result instanceof Error) $result->halt(); + if ($result instanceof Error) throw new YayPreprocessorError($result->message()); return $result->as($this->label); } @@ -815,7 +811,7 @@ function ns() : Parser function ls(Parser $parser, Parser $delimiter, int $flags = LS_DISCARD_DELIMITER) : Parser { if (! $parser->isFallible()) - throw new InvalidArgumentException( + throw new YayPreprocessorError( 'Infinite loop at ' . __FUNCTION__ . '('. $parser . '(*))'); return new class(__FUNCTION__, $parser, $delimiter, $flags) extends Parser @@ -866,7 +862,7 @@ function isFallible() : bool function lst(Parser $parser, Parser $delimiter, int $flags = LS_DISCARD_DELIMITER) : Parser { if (! $parser->isFallible()) - throw new InvalidArgumentException( + throw new YayPreprocessorError( 'Infinite loop at ' . __FUNCTION__ . '('. $parser . '(*))'); return new class(__FUNCTION__, $parser, $delimiter, $flags) extends Parser @@ -962,7 +958,7 @@ function optimize() : Parser private function preventCircularPointerDereference() { if (pointer::class === $this->type && $this->stack[0] === $this->stack[0]()->stack[0]) - throw new \Exception("Circular pointer dereference at {$this}."); + throw new YayPreprocessorError("Circular pointer dereference at {$this}."); } }; } @@ -1060,7 +1056,7 @@ function midrule(callable $midrule, bool $isFallible = true, Expected $expected { function parser(TokenStream $ts) /*: Result|null*/ { - $result = $this->stack[0]($ts); + $result = $this->stack[0]($ts, $this->label); if ($result instanceof Ast) $result->as($this->label); diff --git a/src/parsers_internal.php b/src/parsers_internal.php index a1172fe..843a8ee 100644 --- a/src/parsers_internal.php +++ b/src/parsers_internal.php @@ -75,6 +75,43 @@ function token_constant() : Parser { return rtoken('/^T_\w+$/')->as('token_constant'); } +function array_arg(): Parser { + $string = string()->as('string'); + $int = token(T_LNUMBER)->as('int'); + return $array = + chain( + token('[') + , + commit( + optional( + lst( + either( + chain( + either($int, $string)->as('key'), + token(T_DOUBLE_ARROW), + either( + chain($int)->as('value'), + chain($string)->as('value'), + chain(pointer($array))->as('value') + ) + ) + ->as('key_value_pair') + , + chain($int)->as('value'), + chain($string)->as('value'), + chain(pointer($array))->as('value') + ), + token(',') + ) + ->as('values') + ) + ) + , + token(']') + ) + ->as('array'); +} + function parsec() : Parser { return $parser = @@ -114,6 +151,8 @@ function parsec() : Parser { sigil(token(T_STRING, 'this'))->as('this') , label()->as('literal') + , + array_arg()->as('array') ) , token(',') @@ -150,10 +189,10 @@ function label_or_array_access() : Parser { // becomes a single Ast path string like `T_STRING(foo bar baz)` $ast->__construct( $ast->label(), - [ - 'name' => new Token(T_STRING, str_replace(['[', ']'], [' ', ''], $ast->implode()), $ast->tokens()[0]->line()), - 'complex_name' => new Token(T_STRING, $ast->implode(), $ast->tokens()[0]->line()), - 'complex' => (bool) $ast->complex, + $ast->unwrap() + [ + '_name' => new Token(T_STRING, str_replace(['[', ']'], [' ', ''], $ast->implode()), $ast->tokens()[0]->line()), + '_complex_name' => new Token(T_STRING, $ast->implode(), $ast->tokens()[0]->line()), + '_complex' => (bool) $ast->complex, ] ); }) diff --git a/tests/AstTest.php b/tests/AstTest.php index e6ec81b..0c8fa9a 100644 --- a/tests/AstTest.php +++ b/tests/AstTest.php @@ -45,7 +45,7 @@ function providerForTestMapAstCastOnFailure() { * @dataProvider providerForTestMapAstCastOnFailure */ function testMapAstCastOnFailure(string $path, string $castMethod, string $typeName) { - $this->expectException(YayException::class); + $this->expectException(YayPreprocessorError::class); $this->expectExceptionMessageRegExp("/^Ast cannot be casted to '{$typeName}'$/"); $ast = new Ast('', ['defined' => true]); var_dump($ast->{$path}->$castMethod()); @@ -105,4 +105,47 @@ function testAstFlattenning() { $this->assertEquals([$token1, $token2], $flattened->array()); } + + function testAstSet() { + $ast = new Ast('label', [ + 'deep' => [ + 'token' => new Token(T_STRING, 'foo'), + 'deeper' => [ + 'tokens' => [0 => new Token(T_STRING, 'bar'), 1 => new Token(T_STRING, 'baz')], + ], + ], + ]); + + $ast->set('deep token', $patchedFooToken = new Token(T_STRING, 'patched_foo')); + + $ast->{'deep deeper tokens 0'} = $patchedBarToken = new Token(T_STRING, 'patched_bar'); + $ast->{'deep deeper tokens 1'} = $patchedBazToken = new Token(T_STRING, 'patched_baz'); + + $expected = [ + 'deep' => [ + 'token' => $patchedFooToken, + 'deeper' => [ + 'tokens' => [0 => $patchedBarToken, 1 => $patchedBazToken], + ], + ], + ]; + + $this->assertSame($expected, $ast->unwrap()); + $this->assertSame($expected, $ast->array()); + } + + function testAstHiddenNodes() { + $exposed = new Token(T_STRING, 'exposed'); + $hidden = new Token(T_STRING, '_hidden'); + $ast = new Ast('', [ + 'exposed' => $exposed, + '_hidden' => $hidden, + 'deep' => [ + 'exposed' => $exposed, + '_hidden' => $hidden, + ] + ]); + $this->assertSame([$exposed, $exposed], $ast->tokens()); + $this->assertSame('exposed,exposed', $ast->implode(',')); + } } diff --git a/tests/MacroScopeTest.php b/tests/MacroScopeTest.php index 369b3be..1a67ac5 100644 --- a/tests/MacroScopeTest.php +++ b/tests/MacroScopeTest.php @@ -8,25 +8,16 @@ class MacroScopeTest extends \PHPUnit\Framework\TestCase { const - FIXTURES_DIR = 'fixtures/scope', - ABSOLUTE_FIXTURES_DIR = __DIR__ . '/' . self::FIXTURES_DIR + ABSOLUTE_FIXTURES_DIR = __DIR__ . '/fixtures/scope' ; - function setUp() { - StreamWrapper::register(new Engine); - } - function testGlobalMacro() { - include 'yay://' . self::ABSOLUTE_FIXTURES_DIR . '/macros.php'; - $result = include 'yay://' . self::ABSOLUTE_FIXTURES_DIR . '/test_global.php'; - $this->assertTrue($result); - } + $engine = new Engine; - function testLocalMacro() { - include 'yay://' . self::ABSOLUTE_FIXTURES_DIR . '/macros.php'; - $result = include 'yay://' . self::ABSOLUTE_FIXTURES_DIR . '/test_local.php'; - $this->assertEquals('LOCAL_MACRO', $result); - } + $source = str_replace('expand(file_get_contents(self::ABSOLUTE_FIXTURES_DIR . '/macros.php'))); + $this->assertSame([true, true], eval($source)); - function tearDown() { StreamWrapper::unregister(); } + $source = str_replace('expand(file_get_contents(self::ABSOLUTE_FIXTURES_DIR . '/run.php'))); + $this->assertSame([true, 'LOCAL_MACRO'], eval($source)); + } } diff --git a/tests/ParserTest.php b/tests/ParserTest.php index a19375d..d2029e0 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -18,7 +18,7 @@ function tearDown() } protected function parseHalt(TokenStream $ts, Parser $parser, $msg) { - $this->expectException(Halt::class); + $this->expectException(YayPreprocessorError::class); $this->expectExceptionMessage(implode(PHP_EOL, (array) $msg)); $current = $ts->current(); @@ -32,7 +32,7 @@ function($commit) use($parser) { ->withErrorLevel(Error::ENABLED) ->parse($ts); } - catch (Halt $e) { + catch (YayPreprocessorError $e) { $this->assertSame($current, $ts->current(), 'Failed to backtrack.'); throw $e; @@ -379,7 +379,7 @@ function testChainOnEnd() { } /** - * @expectedException \InvalidArgumentException + * @expectedException \Yay\YayPreprocessorError * @expectedExceptionMessage Dead Yay\token() parser at Yay\either(...[2]) */ function testEitherDeadParserDetection() { diff --git a/tests/SpecsTest.php b/tests/SpecsTest.php index 8b306f9..3597132 100644 --- a/tests/SpecsTest.php +++ b/tests/SpecsTest.php @@ -112,9 +112,11 @@ function run() { $stmts = $parser->parse($this->out); $this->out = $prettyPrinter->prettyPrintFile($stmts) . PHP_EOL . PHP_EOL . '?>'; } - } catch(YayParseError $e) { + } catch(YayPreprocessorError $e) { $this->out = $e->getMessage(); // $this->out = (string) $e; + } catch(YayRuntimeException $e) { + $this->out = $e->getMessage(); } catch(\PhpParser\Error $e){ $this->out = 'PHP ' . $e->getMessage(); // $this->out = (string) $e; diff --git a/tests/StreamWrapperTest.php b/tests/StreamWrapperTest.php deleted file mode 100644 index 792e7ae..0000000 --- a/tests/StreamWrapperTest.php +++ /dev/null @@ -1,54 +0,0 @@ -expectException(\ParseError::class); - $this->expectExceptionMessage($error); - include 'yay://' . $file; - } - - function testStreamWrapperInclusionRelative() { - include 'yay://' . self::FIXTURES_DIR . '/type_alias.php'; - $result = \Yay\Fixtures\Wrapper\test_type_alias(__FILE__); - $this->assertEquals('pass', $result); - } - - function testStreamWrapperInclusionAbsolute() { - include 'yay://' . self::ABSOLUTE_FIXTURES_DIR . '/type_alias_absolute.php'; - $result = \Yay\Fixtures\Wrapper\test_type_alias_absolute(__FILE__); - $this->assertEquals('pass', $result); - } - - function tearDown() { StreamWrapper::unregister(); } -} diff --git a/tests/fixtures/expanders.php b/tests/fixtures/expanders.php index a500e6d..b80c52c 100644 --- a/tests/fixtures/expanders.php +++ b/tests/fixtures/expanders.php @@ -2,14 +2,48 @@ use Yay\{Token, TokenStream}; -function my_hello_expander(TokenStream $ts) : TokenStream { +function my_hello_tokenstream_expander(TokenStream $ts) : TokenStream { $str = str_replace("'", "\'", (string) $ts); return TokenStream::fromSequence( new Token( - T_CONSTANT_ENCAPSED_STRING, "'Hello, {$str}'", $ts->first()->line() + T_CONSTANT_ENCAPSED_STRING, "'Hello, {$str}. From TokenStream.'", $ts->first()->line() ) ) ; } + +function my_cheers_tokenstream_expander(TokenStream $ts) : TokenStream { + $str = str_replace("'", "", (string) $ts); + + return + TokenStream::fromSequence( + new Token( + T_CONSTANT_ENCAPSED_STRING, "'{$str} Cheers!'", $ts->first()->line() + ) + ) + ; +} + +function my_hello_ast_expander(\Yay\Ast $ast) : \Yay\Ast { + return new \Yay\Ast($ast->label(), new Token( + T_CONSTANT_ENCAPSED_STRING, "'Hello, {$ast->token()}. From Ast.'", $ast->token()->line() + )); +} + +function my_cheers_ast_expander(\Yay\Ast $ast) : \Yay\Ast { + $str = str_replace("'", "", (string) $ast->token()); + + return new \Yay\Ast($ast->label(), new Token( + T_CONSTANT_ENCAPSED_STRING, "'{$str} Cheers!'", $ast->token()->line() + )); +} + +function my_foo_expander(\Yay\Ast $ast) { + return TokenStream::fromSourceWithoutOpenTag(sprintf("'called %s(%s)'", __FUNCTION__, $ast->implode())); +} + +function my_bar_expander(\Yay\Ast $ast) { + return TokenStream::fromSourceWithoutOpenTag(sprintf("'called %s(%s)'", __FUNCTION__, $ast->implode())); +} diff --git a/tests/fixtures/parsers.php b/tests/fixtures/parsers.php index 1a7d71d..297a87b 100644 --- a/tests/fixtures/parsers.php +++ b/tests/fixtures/parsers.php @@ -1,8 +1,17 @@ > { true } $(macro) { 'LOCAL_MACRO' } >> { true } + +return ['GLOBAL_MACRO', 'LOCAL_MACRO']; diff --git a/tests/fixtures/scope/run.php b/tests/fixtures/scope/run.php new file mode 100644 index 0000000..2c60cb2 --- /dev/null +++ b/tests/fixtures/scope/run.php @@ -0,0 +1,3 @@ +> { }; diff --git a/tests/fixtures/wrapper/syntax_error.php b/tests/fixtures/wrapper/syntax_error.php deleted file mode 100644 index f036819..0000000 --- a/tests/fixtures/wrapper/syntax_error.php +++ /dev/null @@ -1 +0,0 @@ -> { - \\$(macro) { - \\$(either(instanceof, token(','), token('('), token(':')) as prec) $(newtype) - } >> { - \\$(prec) $(basetype) - } -} - -type Path = string; - -function test_type_alias(Path $p) : Path { - return 'pass'; -} diff --git a/tests/fixtures/wrapper/type_alias_absolute.php b/tests/fixtures/wrapper/type_alias_absolute.php deleted file mode 100644 index cec2e63..0000000 --- a/tests/fixtures/wrapper/type_alias_absolute.php +++ /dev/null @@ -1,17 +0,0 @@ -> { - \\$(macro) { - \\$(either(instanceof, token(','), token('('), token(':')) as prec) $(newtype) - } >> { - \\$(prec) $(basetype) - } -} - -type Path = string; - -function test_type_alias_absolute(Path $p) : Path { - return 'pass'; -} diff --git a/tests/phpt/errors/bad_dominant_macro_mark_end.phpt b/tests/phpt/errors/bad_dominant_macro_mark_end.phpt index 3f38d85..e452a29 100644 --- a/tests/phpt/errors/bad_dominant_macro_mark_end.phpt +++ b/tests/phpt/errors/bad_dominant_macro_mark_end.phpt @@ -11,4 +11,4 @@ $(macro) { ?> --EXPECTF-- -Bad dominant macro marker '$!' offset 2 on line 4. +Bad dominant macro marker '$!' offset 2, in %s.phpt on line 4. diff --git a/tests/phpt/errors/bad_dominant_macro_mark_start.phpt b/tests/phpt/errors/bad_dominant_macro_mark_start.phpt index 06ed2e5..97a929b 100644 --- a/tests/phpt/errors/bad_dominant_macro_mark_start.phpt +++ b/tests/phpt/errors/bad_dominant_macro_mark_start.phpt @@ -11,4 +11,4 @@ $(macro) { ?> --EXPECTF-- -Bad dominant macro marker '$!' offset 0 on line 4. +Bad dominant macro marker '$!' offset 0, in %s.phpt on line 4. diff --git a/tests/phpt/errors/bad_token_type.phpt b/tests/phpt/errors/bad_token_type.phpt index 825ecb3..c12de3b 100644 --- a/tests/phpt/errors/bad_token_type.phpt +++ b/tests/phpt/errors/bad_token_type.phpt @@ -7,4 +7,4 @@ $(macro) { $(T_HAKUNAMATATA as foo) } >> { }; ?> --EXPECTF-- -Undefined token type 'T_HAKUNAMATATA' on line 3. +Undefined token type 'T_HAKUNAMATATA', in %s.phpt on line 3. diff --git a/tests/phpt/errors/capture_redefinition.phpt b/tests/phpt/errors/capture_redefinition.phpt index 86154ae..6e44930 100644 --- a/tests/phpt/errors/capture_redefinition.phpt +++ b/tests/phpt/errors/capture_redefinition.phpt @@ -11,4 +11,4 @@ $(macro) { ?> --EXPECTF-- -Redefinition of macro capture identifier 'foo' on line 4. +Redefinition of macro capture identifier 'foo', in %s.phpt on line 4. diff --git a/tests/phpt/errors/capture_without_type.phpt b/tests/phpt/errors/capture_without_type.phpt index 3f6318d..9040d82 100644 --- a/tests/phpt/errors/capture_without_type.phpt +++ b/tests/phpt/errors/capture_without_type.phpt @@ -7,4 +7,4 @@ $(macro) { $(foo) } >> { $(foo) } ?> --EXPECTF-- -Bad macro capture identifier '$(foo)' on line 3. +Bad macro capture identifier '$(foo)', in %s.phpt on line 3. diff --git a/tests/phpt/errors/empty_expander_template.phpt b/tests/phpt/errors/empty_expander_template.phpt deleted file mode 100644 index e1701fe..0000000 --- a/tests/phpt/errors/empty_expander_template.phpt +++ /dev/null @@ -1,16 +0,0 @@ ---TEST-- -Empty expander template ---FILE-- -> { - $$(concat()) // forgot to pass $(foo) -} - -SOME_T_STRING; - -?> ---EXPECTF-- -Empty expander slice on 'concat()' at line 6. diff --git a/tests/phpt/errors/error_line_number.phpt b/tests/phpt/errors/error_line_number.phpt index afa048e..833fded 100644 --- a/tests/phpt/errors/error_line_number.phpt +++ b/tests/phpt/errors/error_line_number.phpt @@ -29,4 +29,4 @@ foo; // L:25 ?> --EXPECTF-- -Unexpected T_STRING(foo) on line 25, expected T_STRING(expected). +Unexpected T_STRING(foo), in %s.phpt on line 25, expected T_STRING(expected). diff --git a/tests/phpt/errors/expansion_undeclared.phpt b/tests/phpt/errors/expansion_undeclared.phpt index 8ce2d23..002a097 100644 --- a/tests/phpt/errors/expansion_undeclared.phpt +++ b/tests/phpt/errors/expansion_undeclared.phpt @@ -16,7 +16,7 @@ $a >> $b; ?> --EXPECTF-- -Undefined macro expansion 'baz' on line 7 with context: [ +Undefined macro expansion 'baz', in %s.phpt on line 7 with context: [ "foo", 0, "bar" diff --git a/tests/phpt/errors/macro_empty_pattern.phpt b/tests/phpt/errors/macro_empty_pattern.phpt index ddfcff6..fbbab07 100644 --- a/tests/phpt/errors/macro_empty_pattern.phpt +++ b/tests/phpt/errors/macro_empty_pattern.phpt @@ -11,4 +11,4 @@ $(macro) { ?> --EXPECTF-- -Empty macro pattern on line 3. +Empty macro pattern, in %s.phpt on line 3. diff --git a/tests/phpt/errors/macro_without_expansion.phpt b/tests/phpt/errors/macro_without_expansion.phpt index 3811b34..b50aed5 100644 --- a/tests/phpt/errors/macro_without_expansion.phpt +++ b/tests/phpt/errors/macro_without_expansion.phpt @@ -7,4 +7,4 @@ $(macro) { x } // bad macro, missing expansion section ?> --EXPECTF-- -Unexpected T_CLOSE_TAG(?>) on line 5, expected T_SR(). +Unexpected T_CLOSE_TAG(?>), in %s.phpt on line 5, expected T_SR(). diff --git a/tests/phpt/errors/missing_expander_argument.phpt b/tests/phpt/errors/missing_expander_argument.phpt new file mode 100644 index 0000000..3f1615f --- /dev/null +++ b/tests/phpt/errors/missing_expander_argument.phpt @@ -0,0 +1,17 @@ +--TEST-- +Test expander call with missing argument +--FILE-- +> { + $$(stringify(/* forgotten $(args) /o\ */)) +} + +match(foo); + +?> +--EXPECTF-- + +TokenStream expander call without tokens `$$(stringify())` as function Yay\DSL\Expanders\stringify(Yay\TokenStream $ts), in %s.phpt on line 6 diff --git a/tests/phpt/expanders/composition_tokenstream_expander.phpt b/tests/phpt/expanders/composition_tokenstream_expander.phpt new file mode 100644 index 0000000..f338a70 --- /dev/null +++ b/tests/phpt/expanders/composition_tokenstream_expander.phpt @@ -0,0 +1,24 @@ +--TEST-- +Uses a custom fully qualified expansion function --pretty-print +--FILE-- +> { + $$(\Yay\tests\fixtures\expanders\my_cheers_tokenstream_expander( + $$(\Yay\tests\fixtures\expanders\my_hello_tokenstream_expander( + $(matched) + )) + )); +} + +hello(Chris); + +?> +--EXPECTF-- + diff --git a/tests/phpt/expanders/error_when_undefined.phpt b/tests/phpt/expanders/error_when_undefined.phpt index 8f5d350..684bab2 100644 --- a/tests/phpt/expanders/error_when_undefined.phpt +++ b/tests/phpt/expanders/error_when_undefined.phpt @@ -13,4 +13,4 @@ yay\undefined(...); ?> --EXPECTF-- -Bad macro expander 'undefined' on line 6. +Bad macro expander 'undefined', in %s.phpt on line 6. diff --git a/tests/phpt/expanders/fully_qualified_expander.phpt b/tests/phpt/expanders/fully_qualified_expander.phpt index 2f666df..6536a4a 100644 --- a/tests/phpt/expanders/fully_qualified_expander.phpt +++ b/tests/phpt/expanders/fully_qualified_expander.phpt @@ -1,12 +1,12 @@ --TEST-- -Uses a custom fully qualified expansion function +Uses a custom fully qualified expansion function --pretty-print --FILE-- > { - $$(\Yay\tests\fixtures\expanders\my_hello_expander($(matched))) + $$(\Yay\tests\fixtures\expanders\my_hello_tokenstream_expander($(matched))); } hello(Chris); @@ -15,6 +15,6 @@ hello(Chris); --EXPECTF-- diff --git a/tests/phpt/expanders/lazy_evaluation_of_expanders.phpt b/tests/phpt/expanders/lazy_evaluation_of_expanders.phpt new file mode 100644 index 0000000..64bb20e --- /dev/null +++ b/tests/phpt/expanders/lazy_evaluation_of_expanders.phpt @@ -0,0 +1,38 @@ +--TEST-- +Only evaluates custom expanders when required --pretty-print +--FILE-- +> { + $(matches ... { + $(fooMatch ? { + $$(\Yay\tests\fixtures\expanders\my_foo_expander($(fooMatch))); + }) + + $(barMatch ? { + $$(\Yay\tests\fixtures\expanders\my_bar_expander($(barMatch))); + }) + }) +} + +foo bar foo + +?> +--EXPECTF-- + diff --git a/tests/phpt/issues/ircmaxell-php-compiler#29.phpt b/tests/phpt/issues/ircmaxell-php-compiler#29.phpt new file mode 100644 index 0000000..549a7dd --- /dev/null +++ b/tests/phpt/issues/ircmaxell-php-compiler#29.phpt @@ -0,0 +1,51 @@ +--TEST-- +Test for bug found at https://github.com/ircmaxell/php-compiler/pull/29 --pretty-print +--FILE-- +> { + $(stmts ... { + $(assignop ? ... { + $(binary ? ... { + $(binary_op ... { + $(binary_xor ? ... { + $(result) = $this->context->builder->bitwiseXor($(binary_left), $__right); + }) + }) + }) + }) + }) +} + +compile { + $result = $value ^ 1; +} + +?> +--EXPECTF-- +Error unpacking a non unpackable Ast node on `$(binary_xor?... {` at line 29 with context: [ + "^" +] + +Hint: use a non ellipsis expansion as in `$(binary_xor ? {` diff --git a/tests/phpt/macro/dominant_macro.phpt b/tests/phpt/macro/dominant_macro.phpt index 5aea4d0..790069b 100644 --- a/tests/phpt/macro/dominant_macro.phpt +++ b/tests/phpt/macro/dominant_macro.phpt @@ -14,4 +14,4 @@ entry_point_b { X => Y -> Z } // fails at "->" ?> --EXPECTF-- -Unexpected T_OBJECT_OPERATOR(->) on line 9, expected '}'. +Unexpected T_OBJECT_OPERATOR(->), in %s.phpt on line 9, expected '}'. diff --git a/tests/phpt/macro/layer_matcher_error_unbalanced.phpt b/tests/phpt/macro/layer_matcher_error_unbalanced.phpt index 476829d..f989a5d 100644 --- a/tests/phpt/macro/layer_matcher_error_unbalanced.phpt +++ b/tests/phpt/macro/layer_matcher_error_unbalanced.phpt @@ -13,4 +13,4 @@ $(macro) { ?> --EXPECTF-- -Unexpected '}' on line 9, expected ']'. +Unexpected '}', in %s.phpt on line 9, expected ']'. diff --git a/tests/phpt/macro/layer_matcher_error_unbalanced_end.phpt b/tests/phpt/macro/layer_matcher_error_unbalanced_end.phpt index 524e407..8ddbd73 100644 --- a/tests/phpt/macro/layer_matcher_error_unbalanced_end.phpt +++ b/tests/phpt/macro/layer_matcher_error_unbalanced_end.phpt @@ -13,4 +13,4 @@ $(macro) { ?> --EXPECTF-- -Unexpected end at T_CLOSE_TAG(?>) on line 11, expected ')'. +Unexpected end at T_CLOSE_TAG(?>), in %s.phpt on line 11, expected ')'. diff --git a/tests/phpt/macro/macro_deep_ast_access_error.phpt b/tests/phpt/macro/macro_deep_ast_access_error.phpt index 7ed062c..5ab048b 100644 --- a/tests/phpt/macro/macro_deep_ast_access_error.phpt +++ b/tests/phpt/macro/macro_deep_ast_access_error.phpt @@ -38,6 +38,6 @@ match { ?> --EXPECTF-- -Undefined macro expansion 'level_a[level_b][level_c][leaf_level_x]' on line 27 with context: [ +Undefined macro expansion 'level_a[level_b][level_c][leaf_level_x]', in %s.phpt on line 27 with context: [ "level_a" ] diff --git a/tests/phpt/macro/operator_if_defined_then_expand_on_error_I.phpt b/tests/phpt/macro/operator_if_defined_then_expand_on_error_I.phpt index 689fd03..9c95925 100644 --- a/tests/phpt/macro/operator_if_defined_then_expand_on_error_I.phpt +++ b/tests/phpt/macro/operator_if_defined_then_expand_on_error_I.phpt @@ -13,7 +13,7 @@ test; ?> --EXPECTF-- -Undefined macro expansion 'undefined' on line 6 with context: [ +Undefined macro expansion 'undefined', in %s.phpt on line 6 with context: [ "foo", 0 ] diff --git a/tests/phpt/macro/operator_if_not_defined_then_expand_on_error_I.phpt b/tests/phpt/macro/operator_if_not_defined_then_expand_on_error_I.phpt index 2f89e20..1bd392d 100644 --- a/tests/phpt/macro/operator_if_not_defined_then_expand_on_error_I.phpt +++ b/tests/phpt/macro/operator_if_not_defined_then_expand_on_error_I.phpt @@ -13,7 +13,7 @@ test; ?> --EXPECTF-- -Undefined macro expansion 'bar' on line 6 with context: [ +Undefined macro expansion 'bar', in %s.phpt on line 6 with context: [ "foo", 0 ] diff --git a/tests/phpt/parsers/array_arg.phpt b/tests/phpt/parsers/array_arg.phpt new file mode 100644 index 0000000..d9c6b81 --- /dev/null +++ b/tests/phpt/parsers/array_arg.phpt @@ -0,0 +1,135 @@ +--TEST-- +Test for constant array as parsec arguments +--FILE-- + 2, 3 => '4', '5' => 6, 'foo' => 'bar', 'baz' => [ + 1 => 2, 3 => '4', '5' => 6, 'foo' => 'bar', 'baz' => 7 + ] + ]) as nested_assoc_array, + \Yay\tests\fixtures\parsers\var_dump_args_parser([[], ['foo' => []], 2 => ['bar' => [1, 2, 3]]]) as random_typing + ) as var_dump) + + ; + +} >> { + $(var_dump) +} + +dummy; + +?> +--EXPECTF-- + + int(1) + [1]=> + string(1) "2" + [2]=> + string(3) "foo" + [3]=> + string(3) "bar" + [4]=> + int(5) + [5]=> + array(0) { + } +} +*//* +nested_non_assoc_array: array(6) { + [0]=> + int(1) + [1]=> + string(1) "2" + [2]=> + string(3) "foo" + [3]=> + string(3) "bar" + [4]=> + int(5) + [5]=> + array(6) { + [0]=> + int(1) + [1]=> + string(1) "2" + [2]=> + string(3) "foo" + [3]=> + string(3) "bar" + [4]=> + int(5) + [5]=> + array(0) { + } + } +} +*//* +nested_assoc_array: array(5) { + [1]=> + int(2) + [3]=> + string(1) "4" + [5]=> + int(6) + ["foo"]=> + string(3) "bar" + ["baz"]=> + array(5) { + [1]=> + int(2) + [3]=> + string(1) "4" + [5]=> + int(6) + ["foo"]=> + string(3) "bar" + ["baz"]=> + int(7) + } +} +*//* +random_typing: array(3) { + [0]=> + array(0) { + } + [1]=> + array(1) { + ["foo"]=> + array(0) { + } + } + [2]=> + array(1) { + ["bar"]=> + array(3) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) + } + } +} +*/ + +?>