# SPDX-License-Identifier: LGPL-2.1-only # Copyright 2022 Jookia <contact@jookia.org> # Parse error reporting consists of the following data structures: # # ParseTasks represent a task such as parsing a file or directive. # # ParseContexts represent a step during the parsing process for the purposes of # error reporting. # A context contains: # - A ParseTask specifying the step # - The Token the task started at (possibly None) # - A parent ParseContext (possibly None) if this task is a component # of another task # # ParseErrors represent an error encountered during a parsing task, such as an # unexpected token or invalid token. # # ParseErrorExceptions represent an error encounted during parsing. # An error exception contains: # - A ParseError detailing the error # - The Token the error is at (possibly None) # - An expected string (possibly None), used if the parser is expecting # a single, specific token such as a keyword # - A ParseContext detailing the current parsing task from hypothesis import given from hypothesis.strategies import composite, integers, sampled_from, text from newlang.i18n import Message from newlang.parse2.parse import ( ParseContext, ParseError, ParseErrorException, ParseTask, format_context, format_exception, format_full_error, ) from tests.templates import template_test_structure from tests.parse2.test_token import draw_token_random, static_token # # Helper functions # # Mapping of error to message identifiers error_message_ids = { ParseError.TEST_ERROR: "ParseErrorTestError", ParseError.NO_TOKEN: "ParseErrorNoToken", ParseError.WRONG_TOKEN: "ParseErrorWrongToken", ParseError.FOUND_STARTTEXT: "ParseErrorFoundStartText", ParseError.FOUND_STARTNOTE: "ParseErrorFoundStartNote", ParseError.NOT_BOOL: "ParseErrorNotBool", ParseError.FOUND_ENDNOTE: "ParseErrorFoundEndNote", ParseError.RESERVED_NAME: "ParseErrorReservedName", ParseError.FOUND_TERMINATOR: "ParseErrorFoundTerminator", } # Mapping of task to message identifiers task_message_ids = { ParseTask.TEST_TASK: "ParseTaskTestTask", ParseTask.PARSE_NOTE: "ParseTaskNote", ParseTask.CLEAR_NOTES: "ParseTaskClearNotes", ParseTask.PARSE_TEXT: "ParseTaskText", ParseTask.PARSE_BOOL: "ParseTaskBool", ParseTask.PARSE_REFERENCE: "ParseTaskReference", ParseTask.PARSE_VALUE: "ParseTaskValue", ParseTask.PARSE_STATEMENT: "ParseTaskStatement", ParseTask.PARSE_SUBJECT: "ParseTaskSubject", ParseTask.PARSE_VERB: "ParseTaskVerb", ParseTask.PARSE_ARGUMENT: "ParseTaskArgument", ParseTask.PARSE_SET: "ParseTaskSet", ParseTask.PARSE_CONDITIONAL: "ParseTaskConditional", ParseTask.PARSE_TEST: "ParseTaskTest", ParseTask.PARSE_SUCCESS: "ParseTaskSuccess", ParseTask.PARSE_FAILURE: "ParseTaskFailure", ParseTask.PARSE_DIRECTIVE: "ParseTaskDirective", ParseTask.PARSE_FILE: "ParseTaskFile", } # Draws a strategy, with 25% of draws being None @composite def draw_maybe(draw, strategy): chance = draw(integers(min_value=1, max_value=4)) if chance == 1: return None else: return draw(strategy) # Draws a random parse task @composite def draw_parse_task(draw): return draw(sampled_from(ParseTask.list())) # Draws a random parse context without a parent @composite def draw_parse_context(draw): task = draw(draw_parse_task()) token = draw(draw_maybe(draw_token_random())) context = draw(draw_maybe(draw_parse_context())) return ParseContext(task, token, context) # Static parse context def static_parse_context(): task = ParseTask.TEST_TASK token = static_token() return ParseContext(task, token, None) # Draws a random parse error @composite def draw_parse_error(draw): return draw(sampled_from(ParseError.list())) # Draws a random parse error exception @composite def draw_parse_error_exception(draw): error = draw(draw_parse_error()) token = draw(draw_maybe(draw_token_random())) expected = draw(draw_maybe(text())) context = draw(draw_parse_context()) return ParseErrorException(error, token, expected, context) # # Test functions # # Test parse context structure @template_test_structure( ParseContext, draw_parse_context(), task=draw_parse_task(), token=draw_maybe(draw_token_random()), parent=draw_maybe(draw_parse_context()), ) def test_parse2_context_structure(): pass # Test parse error exception structure @template_test_structure( ParseErrorException, draw_parse_error_exception(), error=draw_parse_error(), token=draw_maybe(draw_token_random()), expected=draw_maybe(text()), context=draw_maybe(draw_parse_context()), ) def test_parse2_error_exception_structure(): pass # Tests formatting a ParseContext # We expect the following behaviour: # - A Message is returned # - The message ID begins with ParseContext # - The first parameter is task # - If the token field is set, the ID is appended with # "At" and the second and third parameters are the token's # location's line and offset # Two combinations are possible: # - Message("ParseContext", [task]) # - Message("ParseContextAt", [task, file, line, offset]) # task is a message representing the ParseTask, equivalent to: # - Message(task_message_ids[context.task], []) # file is a source file's name # line is a source file's line number # offset is a source file's line offset def _test_parse2_error_format_context(context): task = Message(task_message_ids[context.task], []) has_location = context.token is not None if has_location: file = context.token.location.file line = context.token.location.line offset = context.token.location.offset expected = Message("ParseContextAt", [task, file, line, offset]) else: expected = Message("ParseContext", [task]) value = format_context(context) assert expected == value # Tests formatting with a random ParseContext @given(draw_parse_context()) def test_parse2_error_format_context(context): _test_parse2_error_format_context(context) # Tests formatting with each ParseTask @given(draw_parse_task()) def test_parse2_error_format_parse_task(task): context = ParseContext(task, None, None) _test_parse2_error_format_context(context) # Tests formatting a ParseErrorException # We expect the following behaviour: # - A Message is returned # - The message ID begins with ParseError # - If the expected field is set, the ID is appended with # "Expected" and the first parameter is the expected value # - Otherwise the first parameter is the error # - If the token field is set, the ID is appended with # "At" and the second and third parameters are the token's # location's line and offset # Four combinations are possible: # - Message("ParserError", [error]) # - Message("ParserErrorAt", [error, file, line, offset]) # - Message("ParserErrorExpected", [expected]) # - Message("ParserErrorExpectedAt", [expected, file, line, offset]) # error is a message representing the ParseError, equivalent to: # - Message(error_message_ids[exception.error], []) # file is a source file's name # line is a source file's line number # offset is a source file's line offset def _test_parse2_error_format_exception(exception): has_expected = exception.expected is not None has_location = exception.token is not None # Variables used for message parameters err = Message(error_message_ids[exception.error], []) expect = exception.expected if has_location: file = exception.token.location.file line = exception.token.location.line offset = exception.token.location.offset else: file = None line = None offset = None # Truth table used for message lookup # Indexes are has_expected and has_location messages = [ # Cases without an expected token: [ Message("ParserError", [err]), Message("ParserErrorAt", [err, file, line, offset]), ], # Cases with an expected token: [ Message("ParserErrorExpected", [expect]), Message("ParserErrorExpectedAt", [expect, file, line, offset]), ], ] expected = messages[has_expected][has_location] value = format_exception(exception) assert expected == value # Tests formatting with a random ParseErrorException @given(draw_parse_error_exception()) def test_parse2_error_format_exception(exception): _test_parse2_error_format_exception(exception) # Tests formatting with each ParseError @given(draw_parse_error()) def test_parse2_error_format_parse_error(error): exception = ParseErrorException(error, None, None, static_parse_context()) _test_parse2_error_format_exception(exception) # Tests formatting a full error # We expect the following behaviour: # - An array of Messages are returned # - The first message is the exception formatted # - The second messages is the exception's context formatted # - Subsequent messages are of any context decedents formatted @given(draw_parse_error_exception()) def test_parse2_error_format_full_error(exception): expected = [format_exception(exception)] context = exception.context while context is not None: expected.append(format_context(context)) context = context.parent value = format_full_error(exception) assert expected == value