Skip to content

Commit 11b6426

Browse files
committed
better error reporting, tests
1 parent b2ee74b commit 11b6426

5 files changed

Lines changed: 43 additions & 30 deletions

File tree

classes/local/answer_parser.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ private function is_acceptable_algebraic_formula(bool $fornumericalformula = fal
251251
// formulas, students are allowed to use variables, so the expression is syntactically
252252
// valid and will be interpreted as 'sin*30' which is most certainly wrong. The
253253
// following check will make sure that students do not use function names as variables.
254-
if (array_key_exists($token->value, functions::FUNCTIONS + evaluator::PHPFUNCTIONS)) {
254+
if (self::is_valid_function_name($token->value)) {
255255
return false;
256256
}
257257
/* TODO: maybe we should reject unknown variables, because that avoids mistakes

classes/local/parser.php

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -239,10 +239,14 @@ private function parse_general_expression(?int $stopat = null): expression {
239239
$nextvalue = $nexttoken->value;
240240

241241
// If the current token is a PREFIX and the next one is an IDENTIFIER, we will consider
242-
// that one as a FUNCTION. If the next token has already been classified as a function,
243-
// there is nothing to do. Otherwise, the PREFIX is simply useless and we ignore it.
242+
// that one as a FUNCTION, unless it is not a known function name. If the PREFIX is not
243+
// followed by an identifier, we silently ignore it for maximum backwards compatibility, as
244+
// legacy versions used to remove backslashes from variable definitions.
244245
if ($type === token::PREFIX) {
245-
if ($nexttype === token::IDENTIFIER || $nexttype === token::FUNCTION) {
246+
if ($nexttype === token::IDENTIFIER) {
247+
if (!self::is_valid_function_name($nextvalue)) {
248+
$this->die(get_string('error_prefix', 'qtype_formulas'));
249+
}
246250
$nexttype = ($nexttoken->type = token::FUNCTION);
247251
}
248252
}
@@ -264,7 +268,7 @@ private function parse_general_expression(?int $stopat = null): expression {
264268
// break existing questions.
265269
if ($type === token::IDENTIFIER) {
266270
$isnotavariable = !$this->is_known_variable($currenttoken);
267-
$isknownfunction = array_key_exists($value, functions::FUNCTIONS + evaluator::PHPFUNCTIONS);
271+
$isknownfunction = self::is_valid_function_name($value);
268272
$nextisparen = $nexttype === token::OPENING_PAREN;
269273
if ($isnotavariable && $isknownfunction && $nextisparen) {
270274
$type = ($currenttoken->type = token::FUNCTION);
@@ -369,6 +373,16 @@ private function parse_general_expression(?int $stopat = null): expression {
369373
return new expression(shunting_yard::infix_to_rpn($expression));
370374
}
371375

376+
/**
377+
* Check if a given name is a valid function name.
378+
*
379+
* @param string $identifier the name to check
380+
* @return bool
381+
*/
382+
public static function is_valid_function_name(string $identifier): bool {
383+
return array_key_exists($identifier, functions::FUNCTIONS + evaluator::PHPFUNCTIONS);
384+
}
385+
372386
/**
373387
* Retrieve the parsed statements.
374388
*

tests/evaluator_test.php

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1506,8 +1506,8 @@ public static function provide_invalid_assignments(): array {
15061506
'Individual chars of a string cannot be modified.',
15071507
's = "foo"; s[1] = "x"',
15081508
],
1509-
'assignment with invalid function' => [
1510-
"Unknown function: 'idontexist'",
1509+
'prefix with invalid function' => [
1510+
'Syntax error: invalid use of prefix character \.',
15111511
'a = \idontexist(5)',
15121512
],
15131513
'assignment to constant' => [
@@ -2322,6 +2322,22 @@ public function test_impossible_stuff(): void {
23222322
}
23232323
self::assertNotNull($e);
23242324

2325+
// Trying to execute an unknown function should yield the appropriate error message.
2326+
$statement = new expression([
2327+
new token(token::VARIABLE, 'a'),
2328+
new token(token::NUMBER, 5),
2329+
new token(token::NUMBER, 1),
2330+
new token(token::FUNCTION, 'idontexist'),
2331+
new token(token::OPERATOR, '='),
2332+
]);
2333+
$e = null;
2334+
try {
2335+
$evaluator->evaluate($statement);
2336+
} catch (Exception $e) {
2337+
self::assertStringEndsWith("Unknown function: 'idontexist'", $e->getMessage());
2338+
}
2339+
self::assertNotNull($e);
2340+
23252341
// When executing the ternary operator, we must have enough stuff (and the right stuff) on the stack.
23262342
$statement = new expression([
23272343
new token(token::OPERATOR, '?'),
@@ -2350,6 +2366,8 @@ public function test_impossible_stuff(): void {
23502366
self::assertStringEndsWith('Evaluation error: not enough arguments for ternary operator.', $e->getMessage());
23512367
}
23522368
self::assertNotNull($e);
2369+
2370+
23532371
}
23542372

23552373
/**

tests/parser_test.php

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -287,27 +287,4 @@ public function test_impossible_stuff($expected, $input): void {
287287
}
288288
self::assertNotNull($e);
289289
}
290-
291-
/**
292-
* Test that the shunting yard algorithm correctly deals with a PREFIX token
293-
* that might have gotten into the list of tokens.
294-
*/
295-
public function test_shunting_yard_prefix_token(): void {
296-
$tokens = [
297-
new token(token::VARIABLE, 'a'),
298-
new token(token::OPERATOR, '='),
299-
new token(token::PREFIX, '\\'),
300-
new token(token::FUNCTION, 'sin'),
301-
new token(token::OPENING_PAREN, '('),
302-
new token(token::NUMBER, '2'),
303-
new token(token::CLOSING_PAREN, ')'),
304-
];
305-
306-
$output = shunting_yard::infix_to_rpn($tokens);
307-
$output = array_map(function($token) {
308-
return $token->value;
309-
}, $output);
310-
311-
self::assertEquals('a,2,1,sin,=', implode(',', $output));
312-
}
313290
}

tests/questiontype_test.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,10 @@ public static function provide_single_part_data_for_form_validation(): array {
324324
'subqtext' => [0 => ['text' => '{_0} and {_0}']],
325325
],
326326
],
327+
[
328+
['answer[0]' => '1:1:Syntax error: invalid use of prefix character \.'],
329+
['answer' => [0 => '\idontexist(20)']],
330+
],
327331
[
328332
['answer[0]' => get_string('error_model_answer_no_content', 'qtype_formulas')],
329333
['answer' => [0 => '#']],

0 commit comments

Comments
 (0)