Skip to content

Commit

Permalink
Fix type parsing in type casts
Browse files Browse the repository at this point in the history
  • Loading branch information
thekid committed Dec 4, 2022
1 parent ba2dc24 commit 990744d
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 10 deletions.
7 changes: 7 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ XP AST ChangeLog

## ?.?.? / ????-??-??

## 9.2.1 / 2022-12-04

* Fixed type parsing in type casts:
- Arrays, maps and generics with nullables, e.g. `(array<?int>)$v`
- Intersection and union types, e.g. `(int|string)$v`.
(@thekid)

## 9.2.0 / 2022-11-12

* Added support for omitting expressions in destructuring assignments,
Expand Down
15 changes: 5 additions & 10 deletions src/main/php/lang/ast/syntax/PHP.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -243,13 +243,7 @@ public function __construct() {
//
// Resolve by looking ahead after the closing ")"
$this->prefix('(', 0, function($parse, $token) {
static $types= [
'<' => true,
'>' => true,
',' => true,
'?' => true,
':' => true
];
static $types= ['<' => 1, '>' => 1, '<<' => 1, '>>' => 1, ',' => 1, '?' => 1, '<?' => 1, ':' => 1, '|' => 1, '&' => 1];

$skipped= [$token, $parse->token];
$cast= true;
Expand All @@ -267,10 +261,11 @@ public function __construct() {
}
$parse->queue= $parse->queue ? array_merge($skipped, $parse->queue) : $skipped;


if ($cast && ('operator' !== $parse->token->kind || '(' === $parse->token->value || '[' === $parse->token->value)) {
$parse->forward();
$parse->expecting('(', 'cast');
$type= $this->type0($parse, false);
$type= $this->type($parse, false);
$parse->expecting(')', 'cast');

return new CastExpression($type, $this->expression($parse, 0), $token->line);
Expand Down Expand Up @@ -1220,7 +1215,7 @@ private function type0($parse, $optional) {

// Resolve ambiguity between short open tag and nullables as in <?int>
if ('<?' === $parse->token->value) {
$parse->queue[]= new Token(self::symbol('?'), '(operator)', '?');
array_unshift($parse->queue, new Token(self::symbol('?'), '(operator)', '?'));
$parse->token->value= '<';
}

Expand All @@ -1244,7 +1239,7 @@ private function type0($parse, $optional) {
if ('>' === $parse->token->symbol->id) {
break;
} else if ('>>' === $parse->token->value) {
$parse->queue[]= $parse->token= new Token(self::symbol('>'));
array_unshift($parse->queue, $parse->token= new Token(self::symbol('>')));
break;
}
} while (',' === $parse->token->value && true | $parse->forward());
Expand Down
78 changes: 78 additions & 0 deletions src/test/php/lang/ast/unittest/parse/CastTest.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php namespace lang\ast\unittest\parse;

use lang\ast\nodes\{CastExpression, Variable};
use lang\ast\types\{IsArray, IsLiteral, IsMap, IsNullable, IsValue, IsGeneric, IsFunction, IsUnion, IsIntersection};
use unittest\{Assert, Test, Values};

class CastTest extends ParseTest {

/** @return iterable */
private function types() {

// Literals
yield ['int', new IsLiteral('int')];
yield ['float', new IsLiteral('float')];
yield ['string', new IsLiteral('string')];
yield ['bool', new IsLiteral('bool')];
yield ['array', new IsLiteral('array')];
yield ['object', new IsLiteral('object')];
yield ['callable', new IsLiteral('callable')];
yield ['iterable', new IsLiteral('iterable')];

// Value types
yield ['Value', new IsValue('Value')];
yield ['\\lang\\Value', new IsValue('\\lang\\Value')];

// Nullable
yield ['?int', new IsNullable(new IsLiteral('int'))];
yield ['?Value', new IsNullable(new IsValue('Value'))];

// Functions
yield ['function(): int', new IsFunction([], new IsLiteral('int'))];
yield ['function(string): void', new IsFunction([new IsLiteral('string')], new IsLiteral('void'))];

// Generic
yield ['List<int>', new IsGeneric(new IsValue('List'), [new IsLiteral('int')])];
yield ['Map<string, Value>', new IsGeneric(new IsValue('Map'), [new IsLiteral('string'), new IsValue('Value')])];
}

#[Test, Values('types')]
public function simple($type, $expected) {
$this->assertParsed(
[new CastExpression($expected, new Variable('a', self::LINE), self::LINE)],
'('.$type.')$a;'
);
}

#[Test, Values('types')]
public function array_of($type, $expected) {
$this->assertParsed(
[new CastExpression(new IsArray($expected), new Variable('a', self::LINE), self::LINE)],
'(array<'.$type.'>)$a;'
);
}

#[Test, Values('types')]
public function map_of($type, $expected) {
$this->assertParsed(
[new CastExpression(new IsMap(new IsLiteral('string'), $expected), new Variable('a', self::LINE), self::LINE)],
'(array<string, '.$type.'>)$a;'
);
}

#[Test, Values('types')]
public function union_of($type, $expected) {
$this->assertParsed(
[new CastExpression(new IsUnion([new IsLiteral('string'), $expected]), new Variable('a', self::LINE), self::LINE)],
'(string|'.$type.')$a;'
);
}

#[Test, Values('types')]
public function intersection_of($type, $expected) {
$this->assertParsed(
[new CastExpression(new IsIntersection([new IsLiteral('string'), $expected]), new Variable('a', self::LINE), self::LINE)],
'(string&'.$type.')$a;'
);
}
}

0 comments on commit 990744d

Please sign in to comment.