Skip to content

Commit b476e8b

Browse files
authored
Merge pull request #208 from xp-framework/fix/errors-and-stack
Ensure filenames and lines match up to parsed code
2 parents 7f336aa + ceb9912 commit b476e8b

File tree

5 files changed

+169
-59
lines changed

5 files changed

+169
-59
lines changed

src/main/php/xp/runtime/Code.class.php

Lines changed: 55 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,58 +9,66 @@
99
* @test xp://net.xp_framework.unittest.runtime.CodeTest
1010
*/
1111
class Code {
12-
private $fragment;
13-
private $imports= [];
14-
private $modules= [];
15-
private $namespace= null;
12+
private $name, $fragment, $modules, $line, $imports, $namespace;
13+
14+
static function __static() {
15+
stream_wrapper_register('script', Script::class);
16+
}
1617

1718
/**
1819
* Creates a new code instance
1920
*
2021
* @param string $input
22+
* @param string $name
2123
*/
22-
public function __construct($input) {
24+
public function __construct($input, $name= '(unnamed)') {
25+
$this->name= $name;
26+
$this->namespace= null;
27+
$this->modules= new Modules();
28+
$this->imports= [];
29+
30+
$pos= 0;
31+
$length= strlen($input);
2332

2433
// Shebang
25-
if (0 === strncmp($input, '#!', 2)) {
26-
$input= substr($input, strcspn($input, "\n") + 1);
34+
if ($pos < $length && 0 === substr_compare($input, '#!', $pos, 2)) {
35+
$pos+= strcspn($input, "\n", $pos) + 1;
2736
}
2837

2938
// PHP open tags
30-
if (0 === strncmp($input, '<?', 2)) {
31-
$input= substr($input, strcspn($input, "\r\n\t =") + 1);
39+
if ($pos < $length && 0 === substr_compare($input, '<?', $pos, 2)) {
40+
$pos+= strcspn($input, "\r\n\t =", $pos) + 1;
3241
}
3342

34-
$this->fragment= trim($input, "\r\n\t ;").';';
43+
// Trim whitespace on the left
44+
$pos+= strspn($input, "\r\n\t ", $pos);
3545

36-
if (0 === strncmp($this->fragment, 'namespace', 9)) {
37-
$length= strcspn($this->fragment, ';', 10);
38-
$this->namespace= substr($this->fragment, 10, $length);
39-
$this->fragment= ltrim(substr($this->fragment, 11 + $length), "\r\n\t ");
46+
// Parse namespace declaration
47+
if ($pos < $length && 0 === substr_compare($input, 'namespace', $pos, 9)) {
48+
$l= strcspn($input, ';', $pos);
49+
$this->namespace= substr($input, $pos + 10, $l - 10);
50+
$pos+= $l + 1;
51+
$pos+= strspn($input, "\r\n\t ", $pos);
4052
}
4153

42-
$this->modules= new Modules();
43-
while (0 === strncmp($this->fragment, 'use ', 4)) {
44-
$delim= strpos($this->fragment, ';');
45-
foreach ($this->importsIn(substr($this->fragment, 4, $delim - 4)) as $import => $module) {
54+
// Parse imports
55+
while ($pos < $length && 0 === substr_compare($input, 'use ', $pos, 4)) {
56+
$l= strcspn($input, ';', $pos);
57+
foreach ($this->importsIn(substr($input, $pos + 4, $l - 4)) as $import => $module) {
4658
$this->imports[]= $import;
4759
$module && $this->modules->add($module);
4860
}
49-
$this->fragment= ltrim(substr($this->fragment, $delim + 1), "\r\n\t ");
61+
$pos+= $l + 1;
62+
$pos+= strspn($input, "\r\n\t ", $pos);
5063
}
64+
65+
$this->fragment= rtrim(substr($input, $pos), "\r\n\t ;").';';
66+
$this->line= 0 === $pos ? 0 : substr_count($input, "\n", 0, $pos > $length ? $length : $pos);
5167
}
5268

5369
/** @return string */
5470
public function fragment() { return $this->fragment; }
5571

56-
/** @return string */
57-
public function expression() {
58-
return strstr($this->fragment, 'return ') || strstr($this->fragment, 'return;')
59-
? $this->fragment
60-
: 'return '.$this->fragment
61-
;
62-
}
63-
6472
/** @return string[] */
6573
public function imports() { return $this->imports; }
6674

@@ -79,6 +87,20 @@ public function head() {
7987
;
8088
}
8189

90+
/**
91+
* Returns a new instance of this code instance, with a `return` statement
92+
* inserted if necessary.
93+
*
94+
* @return self
95+
*/
96+
public function withReturn() {
97+
$self= clone $this;
98+
if (!strstr($self->fragment, 'return ') && !strstr($self->fragment, 'return;')) {
99+
$self->fragment= 'return '.$self->fragment;
100+
}
101+
return $self;
102+
}
103+
82104
/**
83105
* Returns types and modules used inside a `use ...` directive.
84106
*
@@ -111,19 +133,21 @@ private function importsIn($use) {
111133
/**
112134
* Runs an expression of code in the context of this code
113135
*
114-
* @param string $expression
115136
* @param string[] $argv
116137
* @return int
117138
* @throws lang.Throwable
118139
*/
119-
public function run($expression, $argv= []) {
140+
public function run($argv= []) {
120141
$this->modules->require();
121142

122-
$argc= sizeof($argv);
143+
Script::$code[$this->name]= '<?php '.$this->head().str_repeat("\n", $this->line).$this->fragment."\nreturn null;";
123144
try {
124-
return eval($this->head().$expression);
145+
$argc= sizeof($argv);
146+
return include('script://'.$this->name);
125147
} catch (\Throwable $t) {
126148
throw Throwable::wrap($t);
149+
} finally {
150+
unset(Script::$code[$this->name]);
127151
}
128152
}
129153
}

src/main/php/xp/runtime/Dump.class.php

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php namespace xp\runtime;
22

3-
use util\cmd\Console;
43
use lang\XPClass;
4+
use util\cmd\Console;
55

66
/**
77
* Evaluates code and dumps its output.
@@ -11,22 +11,23 @@ class Dump {
1111
/**
1212
* Main
1313
*
14-
* @param string[] args
14+
* @param string[] $args
15+
* @return int
1516
*/
1617
public static function main(array $args) {
1718
$way= array_shift($args);
1819

1920
// Read sourcecode from STDIN if no further argument is given
2021
if (empty($args)) {
21-
$code= new Code(file_get_contents('php://stdin'));
22+
$code= new Code(file_get_contents('php://stdin'), '(standard input)');
2223
} else if ('--' === $args[0]) {
23-
$code= new Code(file_get_contents('php://stdin'));
24+
$code= new Code(file_get_contents('php://stdin'), '(standard input)');
2425
} else {
25-
$code= new Code($args[0]);
26+
$code= new Code($args[0], '(command line argument)');
2627
}
2728

2829
// Perform
29-
$return= $code->run($code->expression(), [XPClass::nameOf(self::class)] + $args);
30+
$return= $code->withReturn()->run([XPClass::nameOf(self::class)] + $args);
3031
switch ($way) {
3132
case '-w': Console::writeLine($return); break;
3233
case '-d': var_dump($return); break;

src/main/php/xp/runtime/Evaluate.class.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ public static function main(array $args) {
1717

1818
// Read sourcecode from STDIN if no further argument is given
1919
if (empty($args)) {
20-
$code= new Code(file_get_contents('php://stdin'));
20+
$code= new Code(file_get_contents('php://stdin'), '(standard input)');
2121
} else if ('--' === $args[0]) {
22-
$code= new Code(file_get_contents('php://stdin'));
22+
$code= new Code(file_get_contents('php://stdin'), '(standard input)');
2323
} else if (is_file($args[0])) {
24-
$code= new Code(file_get_contents($args[0]));
24+
$code= new Code(file_get_contents($args[0]), $args[0]);
2525
} else {
26-
$code= new Code($args[0]);
26+
$code= new Code($args[0], '(command line argument)');
2727
}
2828

29-
return $code->run($code->fragment(), [XPClass::nameOf(self::class)] + $args);
29+
return $code->run([XPClass::nameOf(self::class)] + $args);
3030
}
3131
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php namespace xp\runtime;
2+
3+
class Script {
4+
public static $code= [];
5+
private $stream, $offset;
6+
7+
/**
8+
* Opens path
9+
*
10+
* @param string $path
11+
* @param string $mode
12+
* @param int $options
13+
* @param string $opened
14+
*/
15+
public function stream_open($path, $mode, $options, &$opened) {
16+
sscanf($path, "%[^:]://%[^\r]", $scheme, $opened);
17+
if (isset(self::$code[$opened])) {
18+
$this->stream= self::$code[$opened];
19+
$this->offset= 0;
20+
return true;
21+
}
22+
23+
return false;
24+
}
25+
26+
/**
27+
* Reads bytes
28+
*
29+
* @param int $count
30+
* @return string
31+
*/
32+
public function stream_read($count) {
33+
$chunk= substr($this->stream, $this->offset, $count);
34+
$this->offset+= $count;
35+
return $chunk;
36+
}
37+
38+
/** @return [:var] */
39+
public function stream_stat() {
40+
return ['size' => strlen($this->stream)];
41+
}
42+
43+
/** @return bool */
44+
public function stream_eof() {
45+
return $this->offset >= strlen($this->stream);
46+
}
47+
48+
/** @return void */
49+
public function stream_close() {
50+
// NOOP
51+
}
52+
}

src/test/php/net/xp_framework/unittest/runtime/CodeTest.class.php

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,17 @@ public function fragment_with_php_tag($input) {
3636

3737
#[@test]
3838
public function expression() {
39-
$this->assertEquals('return "Test";', (new Code('"Test"'))->expression());
39+
$this->assertEquals('return "Test";', (new Code('"Test"'))->withReturn()->fragment());
4040
}
4141

4242
#[@test]
4343
public function expression_with_semicolon() {
44-
$this->assertEquals('return "Test";', (new Code('"Test";'))->expression());
44+
$this->assertEquals('return "Test";', (new Code('"Test";'))->withReturn()->fragment());
4545
}
4646

4747
#[@test]
4848
public function expression_with_existing_return() {
49-
$this->assertEquals('return "Test";', (new Code('return "Test";'))->expression());
49+
$this->assertEquals('return "Test";', (new Code('return "Test";'))->withReturn()->fragment());
5050
}
5151

5252
#[@test, @values([
@@ -63,20 +63,6 @@ public function use_is_stripped_from_fragment($input) {
6363
$this->assertEquals('test();', (new Code($input))->fragment());
6464
}
6565

66-
#[@test, @values([
67-
# 'use util\Date; test()',
68-
# 'use util\Date, util\TimeZone; test()',
69-
# 'use util\Date; use util\TimeZone; test()',
70-
# 'use util\{Date, TimeZone}; test()',
71-
# ' use util\Date; test()',
72-
# '<?php use util\Date; test()',
73-
# '<?php use util\Date; test()',
74-
# "<?php\nuse util\Date; test()"
75-
#])]
76-
public function use_is_stripped_from_expression($input) {
77-
$this->assertEquals('return test();', (new Code($input))->expression());
78-
}
79-
8066
#[@test]
8167
public function empty_code_has_no_imports() {
8268
$this->assertEquals([], (new Code(''))->imports());
@@ -160,4 +146,51 @@ public function modules_for_code_with_import_without_module() {
160146
public function modules_for_code_with_import_from_module() {
161147
$this->assertEquals(['xp-forge/sequence'], (new Code('use util\data\Sequence from "xp-forge/sequence";'))->modules()->all());
162148
}
149+
150+
#[@test, @values([
151+
# 'return "Test";',
152+
# '<?php return "Test";',
153+
# "<?php\nreturn 'Test';",
154+
# "<?php namespace test;\nreturn 'Test';",
155+
#])]
156+
public function run($input) {
157+
$code= new Code($input);
158+
$this->assertEquals('Test', $code->run());
159+
}
160+
161+
#[@test]
162+
public function run_without_return() {
163+
$code= new Code('');
164+
$this->assertEquals(null, $code->run());
165+
}
166+
167+
#[@test]
168+
public function code_has_access_to_argv() {
169+
$code= new Code('return $argv;');
170+
$this->assertEquals([1, 2, 3], $code->run([1, 2, 3]));
171+
}
172+
173+
#[@test]
174+
public function code_has_access_to_argc() {
175+
$code= new Code('return $argc;');
176+
$this->assertEquals(3, $code->run([1, 2, 3]));
177+
}
178+
179+
#[@test, @values([
180+
# ['', 1],
181+
# ["<?php\n", 2],
182+
# ["<?php namespace test;\n", 2],
183+
# ["<?php namespace test;\n\nuse util\cmd\Console;\n\n", 5],
184+
#])]
185+
public function errors_reported_with_script_name($head, $line) {
186+
$code= new Code($head.'trigger_error("Test");', 'test.script.php');
187+
$code->run();
188+
189+
$e= ['Test' => ['class' => null, 'method' => 'trigger_error', 'cnt' => 1]];
190+
try {
191+
$this->assertEquals(['test.script.php' => [$line => $e]], \xp::$errors);
192+
} finally {
193+
\xp::gc();
194+
}
195+
}
163196
}

0 commit comments

Comments
 (0)