Repository: hedyorg/hedy

Issue:
Catch ugly parser errors 
The very least we can do is hide them and replace by "oops", but maybe with a bot of string magic we can make them less scary and more readable? They do sometimes help me in debugging with kids.


Discussion:

Resolving patch:
diff --git a/app.py b/app.py
--- a/app.py
+++ b/app.py
@@ -83,7 +83,6 @@ def before_request():
 if not os.getenv('HEROKU_RELEASE_CREATED_AT'):
     logging.warning('Cannot determine release; enable Dyno metadata by running "heroku labs:enable runtime-dyno-metadata -a <APP_NAME>"')
 
-
 @app.route('/parse', methods=['POST'])
 def parse():
     body = request.json
@@ -123,6 +122,13 @@ def parse():
                 error_template = hedy_errors[E.error_code]
                 response["Code"] = "# coding=utf8\n" + E.arguments['fixed_code']
                 response["Warning"] = error_template.format(**E.arguments)
+            elif E.args[0] == "Parse":
+                error_template = hedy_errors[E.error_code]
+                # Localize the names of characters
+                # Localize the names of characters
+                if 'character_found' in E.arguments:
+                    E.arguments['character_found'] = hedy_errors[E.arguments['character_found']]
+                response["Error"] = error_template.format(**E.arguments)
             else:
                 error_template = hedy_errors[E.error_code]
                 response["Error"] = error_template.format(**E.arguments)
diff --git a/hedy.py b/hedy.py
--- a/hedy.py
+++ b/hedy.py
@@ -1,5 +1,5 @@
 from lark import Lark
-from lark.exceptions import VisitError, LarkError
+from lark.exceptions import VisitError, LarkError, UnexpectedEOF
 from lark import Tree, Transformer, Visitor
 from lark.indenter import Indenter
 import sys
@@ -660,7 +660,60 @@ def repair(input_string):
     #the only repair we can do now is remove leading spaces, more can be added!
     return '\n'.join([x.lstrip() for x in input_string.split('\n')])
 
+def translate_characters(s):
+# this method is used to make it more clear to kids what is meant in error messages
+# for example ' ' is hard to read, space is easier
+# this could (should?) be localized so we can call a ' "Hoge komma" for example (Felienne, dd Feb 25, 2021)
+    if s == ' ':
+        return 'space'
+    elif s == ',':
+        return 'comma'
+    elif s == '?':
+        return 'question mark'
+    elif s == '\\n':
+        return 'newline'
+    elif s == '.':
+        return 'period'
+    elif s == '!':
+        return 'exclamation mark'
+    elif s == '*':
+        return 'star'
+    elif s == "'":
+        return 'single quotes'
+    elif s == '"':
+        return 'double quotes'
+    elif s == '/':
+        return 'slash'
+    elif s == '-':
+        return 'dash'
+    elif s >= 'a' and s <= 'z' or s >= 'A' and s <= 'Z':
+        return s
+    else:
+        return s
+
+def filter_and_translate_terminals(list):
+    # in giving error messages, it does not make sense to include
+    # ANONs, and some things like EOL need kid friendly translations
+    new_terminals = []
+    for terminal in list:
+        if terminal[:4] == "ANON":
+            continue
+
+        if terminal == "EOL":
+            new_terminals.append("Newline")
+            break
+
+        #not translated or filtered out? simply add as is:
+        new_terminals.append(terminal)
+
+    return new_terminals
+
+def beautify_parse_error(error_message):
+
+    character_found = error_message.split("'")[1]
+    character_found = translate_characters(character_found)
 
+    return character_found
 
 def transpile_inner(input_string, level):
     if level <= 6:
@@ -674,8 +727,16 @@ def transpile_inner(input_string, level):
             lookup_table = AllAssignmentCommands().transform(abstract_syntaxtree)
 
         except Exception as e:
-            # TODO: if all else fails, here we could translate Lark error messages into more sensible texts!
-            raise HedyException('Parse', level=level, parse_error=e.args[0]) from e
+            try:
+                location = e.line, e.column
+                characters_expected = str(e.allowed)
+                character_found  = beautify_parse_error(e.args[0])
+                # print(e.args[0])
+                # print(location, character_found, characters_expected)
+                raise HedyException('Parse', level=level, location=location, character_found=character_found, characters_expected=characters_expected) from e
+            except UnexpectedEOF:
+                # this one can't be beautified (for now), so give up :)
+                raise e
 
         is_valid = IsValid().transform(program_root)
         if not is_valid[0]:


Test patch:
diff --git a/tests.py b/tests.py
--- a/tests.py
+++ b/tests.py
@@ -130,7 +130,7 @@ def test_two_lines_start_with_spaces(self):
 
 
     def test_transpile_empty(self):
-        with self.assertRaises(hedy.HedyException) as context:
+        with self.assertRaises(Exception) as context:
             result = hedy.transpile("", 1)
 
     def test_transpile_ask(self):
@@ -422,6 +422,19 @@ def test_pront_should_suggest_print(self):
         self.assertEqual('print', str(context.exception.arguments['guessed_command']))
 
 
+    def test_parser_errors_should_be_caught_and_beautified(self):
+        program = """option is ask Rock Paper or Scissors?
+print 'Player 2 ' option
+if option is Scissors
+    print 'Its a tie!'"""
+
+        with self.assertRaises(Exception) as context:
+            result = hedy.transpile(program, 4)
+        self.assertEqual('Parse', str(context.exception))
+
+
+
+
 class TestsLevel5(unittest.TestCase):
     #print should still work
     def test_print_with_var(self):