Skip to content

Commit 8ef7349

Browse files
committed
Ensure exceptions are raised for non-existant callables
See #114 (comment)
1 parent a321734 commit 8ef7349

File tree

7 files changed

+108
-30
lines changed

7 files changed

+108
-30
lines changed
Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,54 @@
11
<?php namespace lang\ast\emit;
22

3+
use lang\ast\Node;
4+
use lang\ast\nodes\{InstanceExpression, ScopeExpression, Literal};
5+
36
/**
4-
* Rewrites callable expressions to regular closures
7+
* Rewrites callable expressions to `Callable::fromClosure()`
58
*
69
* @see https://wiki.php.net/rfc/first_class_callable_syntax
710
*/
811
trait CallablesAsClosures {
912

1013
protected function emitCallable($result, $callable) {
14+
$result->out->write('\Closure::fromCallable(');
15+
if ($callable->expression instanceof Literal) {
1116

12-
// Use variables in the following cases:
13-
//
14-
// $closure(...); => use ($closure)
15-
// $obj->method(...); => use ($obj)
16-
// $obj->$method(...); => use ($obj, $method)
17-
// ($obj->property)(...); => use ($obj)
18-
// $class::$method(...); => use ($class, $method)
19-
// [$obj, 'method'](...); => use ($obj)
20-
// [Foo::class, $method](...); => use ($method)
21-
$use= [];
22-
foreach ($result->codegen->search($callable, 'variable') as $var) {
23-
$use[$var->name]= true;
24-
}
25-
unset($use['this']);
17+
// Rewrite f() => "f"
18+
$result->out->write('"'.trim($callable->expression->expression, '"\'').'"');
19+
} else if ($callable->expression instanceof InstanceExpression) {
20+
21+
// Rewrite $this->f => [$this, "f"]
22+
$result->out->write('[');
23+
$this->emitOne($result, $callable->expression->expression);
24+
if ($callable->expression->member instanceof Literal) {
25+
$result->out->write(',"'.trim($callable->expression->member, '"\'').'"');
26+
} else {
27+
$result->out->write(',');
28+
$this->emitOne($result, $callable->expression->member);
29+
}
30+
$result->out->write(']');
31+
} else if ($callable->expression instanceof ScopeExpression) {
2632

27-
// Create closure
28-
$t= $result->temp();
29-
$result->out->write('function(...'.$t.')');
30-
$use && $result->out->write('use($'.implode(',$', array_keys($use)).')');
31-
$result->out->write('{ return ');
32-
$this->emitOne($result, $callable->expression);
33-
$result->out->write('(...'.$t.'); }');
33+
// Rewrite self::f => ["self", "f"]
34+
$result->out->write('[');
35+
if ($callable->expression->type instanceof Node) {
36+
$this->emitOne($result, $callable->expression->type);
37+
} else {
38+
$result->out->write('"'.$callable->expression->type.'"');
39+
}
40+
if ($callable->expression->member instanceof Literal) {
41+
$result->out->write(',"'.trim($callable->expression->member, '"\'').'"');
42+
} else {
43+
$result->out->write(',');
44+
$this->emitOne($result, $callable->expression->member);
45+
}
46+
$result->out->write(']');
47+
} else {
48+
49+
// Emit other expressions as-is
50+
$this->emitOne($result, $callable->expression);
51+
}
52+
$result->out->write(')');
3453
}
3554
}

src/main/php/lang/ast/emit/PHP.class.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -989,10 +989,8 @@ protected function emitNewClass($result, $new) {
989989
}
990990

991991
protected function emitCallable($result, $callable) {
992-
$t= $result->temp();
993-
$result->out->write('fn(...'.$t.')=>');
994992
$this->emitOne($result, $callable->expression);
995-
$result->out->write('(...'.$t.')');
993+
$result->out->write('(...)');
996994
}
997995

998996
protected function emitInvoke($result, $invoke) {

src/main/php/lang/ast/emit/PHP70.class.php

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php namespace lang\ast\emit;
22

3+
use lang\ast\Node;
4+
use lang\ast\nodes\{InstanceExpression, ScopeExpression, Literal};
35
use lang\ast\types\{IsUnion, IsFunction, IsArray, IsMap, IsNullable, IsValue, IsLiteral};
46

57
/**
@@ -8,7 +10,7 @@
810
* @see https://wiki.php.net/rfc#php_70
911
*/
1012
class PHP70 extends PHP {
11-
use OmitPropertyTypes, OmitConstModifiers, CallablesAsClosures;
13+
use OmitPropertyTypes, OmitConstModifiers;
1214
use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteMultiCatch, RewriteClassOnObjects, RewriteExplicitOctals;
1315

1416
/** Sets up type => literal mappings */
@@ -26,4 +28,51 @@ public function __construct() {
2628
},
2729
];
2830
}
31+
32+
protected function emitCallable($result, $callable) {
33+
$t= $result->temp();
34+
$result->out->write('(is_callable('.$t.'=');
35+
if ($callable->expression instanceof Literal) {
36+
37+
// Rewrite f() => "f"
38+
$result->out->write('"'.trim($callable->expression->expression, '"\'').'"');
39+
} else if ($callable->expression instanceof InstanceExpression) {
40+
41+
// Rewrite $this->f => [$this, "f"]
42+
$result->out->write('[');
43+
$this->emitOne($result, $callable->expression->expression);
44+
if ($callable->expression->member instanceof Literal) {
45+
$result->out->write(',"'.trim($callable->expression->member, '"\'').'"');
46+
} else {
47+
$result->out->write(',');
48+
$this->emitOne($result, $callable->expression->member);
49+
}
50+
$result->out->write(']');
51+
} else if ($callable->expression instanceof ScopeExpression) {
52+
53+
// Rewrite self::f => [self::class, "f"]
54+
$result->out->write('[');
55+
if ($callable->expression->type instanceof Node) {
56+
$this->emitOne($result, $callable->expression->type);
57+
} else {
58+
$result->out->write($callable->expression->type.'::class');
59+
}
60+
if ($callable->expression->member instanceof Literal) {
61+
$result->out->write(',"'.trim($callable->expression->member, '"\'').'"');
62+
} else {
63+
$result->out->write(',');
64+
$this->emitOne($result, $callable->expression->member);
65+
}
66+
$result->out->write(']');
67+
} else {
68+
69+
// Emit other expressions as-is
70+
$this->emitOne($result, $callable->expression);
71+
}
72+
73+
// Emit equivalent of Closure::fromCallable() which doesn't exist until PHP 7.1
74+
$a= $result->temp();
75+
$result->out->write(')?function(...'.$a.') use('.$t.') { return '.$t.'(...'.$a.'); }:');
76+
$result->out->write('(function() { throw new \Error("Given argument is not callable"); })())');
77+
}
2978
}

src/main/php/lang/ast/emit/PHP74.class.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* @see https://wiki.php.net/rfc#php_74
99
*/
1010
class PHP74 extends PHP {
11-
use RewriteBlockLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals;
11+
use RewriteBlockLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, CallablesAsClosures;
1212

1313
/** Sets up type => literal mappings */
1414
public function __construct() {

src/main/php/lang/ast/emit/PHP80.class.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* @see https://wiki.php.net/rfc#php_80
1010
*/
1111
class PHP80 extends PHP {
12-
use RewriteBlockLambdaExpressions, RewriteExplicitOctals;
12+
use RewriteBlockLambdaExpressions, RewriteExplicitOctals, CallablesAsClosures;
1313

1414
/** Sets up type => literal mappings */
1515
public function __construct() {

src/main/php/lang/ast/emit/PHP81.class.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* @see https://wiki.php.net/rfc#php_81
1010
*/
1111
class PHP81 extends PHP {
12-
use RewriteBlockLambdaExpressions;
12+
use RewriteBlockLambdaExpressions, CallablesAsClosures;
1313

1414
/** Sets up type => literal mappings */
1515
public function __construct() {

src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php namespace lang\ast\unittest\emit;
22

3-
use unittest\{Assert, Test, Values};
3+
use lang\Error;
4+
use unittest\{Assert, Expect, Test, Values};
45

56
/**
67
* Tests for first-class callable syntax
@@ -137,4 +138,15 @@ public static function length($arg) { return strlen($arg); }
137138
public function run() { return [self::class, "length"](...); }
138139
}');
139140
}
141+
142+
#[Test, Expect(Error::class), Values(['nonexistant', '$this->nonexistant', 'self::nonexistant', '$nonexistant', '$null'])]
143+
public function non_existant($expr) {
144+
$this->run('class <T> {
145+
public function run() {
146+
$null= null;
147+
$nonexistant= "nonexistant";
148+
return '.$expr.'(...);
149+
}
150+
}');
151+
}
140152
}

0 commit comments

Comments
 (0)