NewLang / tests / parse /
# SPDX-License-Identifier: LGPL-2.1-only
# Copyright 2022 Jookia <>

# 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 src.i18n import Message
from src.parse import (
from tests.templates import template_test_structure
from tests.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",

# Draws a strategy, with 25% of draws being None
def draw_maybe(draw, strategy):
    chance = draw(integers(min_value=1, max_value=4))
    if chance == 1:
        return None
        return draw(strategy)

# Draws a random parse task
def draw_parse_task(draw):
    return draw(sampled_from(list(ParseTask)))

# Draws a random parse context without a parent
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
def draw_parse_error(draw):
    return draw(sampled_from(list(ParseError)))

# Draws a random parse error exception
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
def test_parse_context_structure():

# Test parse error exception structure
def test_parse_error_exception_structure():

# 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])
def test_parse_error_format_context(context):
    task = 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])
        expected = Message("ParseContext", [task])
    value = format_context(context)
    assert expected == value

# 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_parse_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
        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
def test_parse_error_format_exception(exception):

# Tests formatting with each ParseError
def test_parse_error_format_parse_error(error):
    exception = ParseErrorException(error, None, None, static_parse_context())

# 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
def test_parse_error_format_full_error(exception):
    expected = [format_exception(exception)]
    context = exception.context
    while context is not None:
        context = context.parent
    value = format_full_error(exception)
    assert expected == value