diff --git a/packages/bolt/src/bolt/ast.py b/packages/bolt/src/bolt/ast.py index 34f75a7e9..3b756495e 100644 --- a/packages/bolt/src/bolt/ast.py +++ b/packages/bolt/src/bolt/ast.py @@ -75,6 +75,7 @@ AstChildren, AstCommand, AstCommandSentinel, + AstError, AstJson, AstLiteral, AstNode, @@ -493,7 +494,7 @@ class AstProcMacroMarker(AstNode): class AstProcMacroResult(AstNode): """Ast proc macro result node.""" - commands: AstChildren[AstCommand] = required_field() + commands: AstChildren[AstCommand|AstError] = required_field() @dataclass(frozen=True, slots=True) diff --git a/packages/bolt/src/bolt/contrib/sandbox.py b/packages/bolt/src/bolt/contrib/sandbox.py index 567a2be99..d08061f5f 100644 --- a/packages/bolt/src/bolt/contrib/sandbox.py +++ b/packages/bolt/src/bolt/contrib/sandbox.py @@ -189,7 +189,11 @@ def activate(self): get_attribute_handler = self.runtime.helpers["get_attribute_handler"] self.runtime.helpers.update( +<<<<<<< HEAD get_attribute_handler=lambda obj: SandboxedAttributeHandler( +======= + get_attribute_handler=lambda obj: SandboxedAttributeHandler( # type: ignore +>>>>>>> bolt-fork/migrate-to-beet obj=obj, handler=get_attribute_handler(obj), sandbox=self, diff --git a/packages/bolt/src/bolt/emit.py b/packages/bolt/src/bolt/emit.py index ded049d89..8d9fbbc9e 100644 --- a/packages/bolt/src/bolt/emit.py +++ b/packages/bolt/src/bolt/emit.py @@ -6,7 +6,7 @@ from contextlib import contextmanager from typing import Any, Callable, Generator, Iterator, List, Optional, ParamSpec, Tuple -from mecha import AstCommand, AstNode, AstRoot +from mecha import AstCommand, AstError, AstNode, AstRoot from .utils import internal @@ -16,7 +16,7 @@ class CommandEmitter: """Command emitter.""" - commands: List[AstCommand] + commands: List[AstCommand|AstError] nesting: List[Tuple[str, Tuple[AstNode, ...]]] def __init__(self): @@ -26,8 +26,8 @@ def __init__(self): @contextmanager def scope( self, - commands: Optional[List[AstCommand]] = None, - ) -> Iterator[List[AstCommand]]: + commands: Optional[List[AstCommand|AstError]] = None, + ) -> Iterator[List[AstCommand|AstError]]: """Create a new scope to gather commands.""" if commands is None: commands = [] @@ -46,7 +46,7 @@ def capture_output( f: Callable[P, Any], *args: P.args, **kwargs: P.kwargs, - ) -> List[AstCommand]: + ) -> List[AstCommand|AstError]: """Invoke a user-defined function and return the list of generated commands.""" with self.scope() as output: result = f(*args, **kwargs) diff --git a/packages/bolt/src/bolt/parse.py b/packages/bolt/src/bolt/parse.py index f521faa42..72e0c859f 100644 --- a/packages/bolt/src/bolt/parse.py +++ b/packages/bolt/src/bolt/parse.py @@ -76,6 +76,7 @@ AdjacentConstraint, AlternativeParser, AstChildren, + AstError, AstCommand, AstJson, AstNode, @@ -968,9 +969,13 @@ def __call__(self, stream: TokenStream) -> Any: def resolve(self, node: AstRoot) -> AstRoot: should_replace = False - commands: List[AstCommand] = [] + commands: List[AstCommand|AstError] = [] for command in node.commands: + if isinstance(command, AstError): + commands.append(command) + continue + stack: List[AstCommand] = [command] while command.arguments and isinstance( @@ -1100,9 +1105,13 @@ def __call__(self, stream: TokenStream) -> AstRoot: stack: List[AstDecorator] = [] changed = False - result: List[AstCommand] = [] + result: List[AstCommand|AstError] = [] for command in node.commands: + if isinstance(command, AstError): + result.append(command) + continue + if isinstance(command, AstStatement) and isinstance( decorator := command.arguments[0], AstDecorator ): @@ -1185,9 +1194,13 @@ def __call__(self, stream: TokenStream) -> Any: return node changed = False - result: List[AstCommand] = [] + result: List[AstCommand|AstError] = [] for command in node.commands: + if isinstance(command, AstError): + result.append(command) + continue + if command.identifier == "return:value" and command.arguments: changed = True @@ -1222,12 +1235,16 @@ def __call__(self, stream: TokenStream) -> AstRoot: node: AstRoot = self.parser(stream) changed = False - result: List[AstCommand] = [] + result: List[AstError|AstCommand] = [] commands = iter(node.commands) previous = "" for command in commands: + if isinstance(command, AstError): + result.append(command) + continue + if command.identifier in ["elif:condition:body", "else:body"]: if previous not in ["if:condition:body", "elif:condition:body"]: exc = InvalidSyntax( @@ -1240,6 +1257,9 @@ def __call__(self, stream: TokenStream) -> AstRoot: elif_chain = [command] for command in commands: + if isinstance(command, AstError): + continue + if command.identifier not in ["elif:condition:body", "else:body"]: break elif_chain.append(command) @@ -1291,6 +1311,9 @@ def __call__(self, stream: TokenStream) -> AstRoot: if not loop: for command in node.commands: + if isinstance(command, AstError): + continue + if command.identifier in ["break", "continue"]: exc = InvalidSyntax( f'Can only use "{command.identifier}" in loops.' @@ -1350,7 +1373,6 @@ def parse_function_signature(stream: TokenStream) -> AstFunctionSignature: stream.expect(("brace", "(")) node = set_location(AstFunctionSignature(name=identifier.value), identifier) - lexical_scope.bind_variable(identifier.value, node) deferred_scope = lexical_scope.deferred(FunctionScope) @@ -1469,6 +1491,7 @@ def parse_function_signature(stream: TokenStream) -> AstFunctionSignature: exc = InvalidSyntax( "Expected at least one named argument after bare variadic marker." ) + raise set_location(exc, argument) return_type_annotation = None @@ -1755,11 +1778,15 @@ class ProcMacroExpansion: def __call__(self, stream: TokenStream) -> AstRoot: should_replace = False - commands: List[AstCommand] = [] + commands: List[AstCommand|AstError] = [] node: AstRoot = self.parser(stream) for command in node.commands: + if isinstance(command, AstError): + commands.append(command) + continue + stack: List[AstCommand] = [command] while command.arguments and isinstance( @@ -2074,9 +2101,13 @@ def __call__(self, stream: TokenStream) -> Any: node: AstRoot = self.parser(stream) changed = False - result: List[AstCommand] = [] + result: List[AstCommand|AstError] = [] for command in node.commands: + if isinstance(command, AstError): + result.append(command) + continue + if ( isinstance(command, AstStatement) and command.arguments @@ -2198,9 +2229,13 @@ def __call__(self, stream: TokenStream) -> AstRoot: def resolve_deferred(self, node: AstRoot, stream: TokenStream) -> AstRoot: should_replace = False - commands: List[AstCommand] = [] + commands: List[AstCommand|AstError] = [] for command in node.commands: + if isinstance(command, AstError): + commands.append(command) + continue + if command.arguments and isinstance( body := command.arguments[-1], AstClassRoot ): @@ -2283,6 +2318,9 @@ def __call__(self, stream: TokenStream) -> AstRoot: if isinstance(node, AstRoot): for command in node.commands: + if isinstance(command, AstError): + continue + if command.identifier in self.command_identifiers: name, _, _ = command.identifier.partition(":") exc = InvalidSyntax( @@ -2402,7 +2440,6 @@ def __call__(self, stream: TokenStream) -> Any: node = AstUnpack(type="dict" if prefix.value == "**" else "list", value=node) return set_location(node, prefix, node.value) - @dataclass class UnpackConstraint: """Constraint for unpacking.""" diff --git a/packages/bolt/src/bolt/runtime.py b/packages/bolt/src/bolt/runtime.py index 831cb5977..3d098149b 100644 --- a/packages/bolt/src/bolt/runtime.py +++ b/packages/bolt/src/bolt/runtime.py @@ -4,7 +4,6 @@ "NonFunctionSerializer", ] - import builtins from dataclasses import dataclass, field from functools import partial @@ -15,6 +14,7 @@ from beet.core.utils import JsonDict, extra_field, required_field from mecha import ( AstRoot, + AstError, CommandSpec, CommandTree, CompilationDatabase, @@ -30,7 +30,7 @@ from pathspec import PathSpec from tokenstream import set_location -from .ast import AstNonFunctionRoot +from .ast import AstNonFunctionRoot, AstRoot from .codegen import Codegen from .emit import CommandEmitter from .helpers import get_bolt_helpers @@ -275,6 +275,10 @@ def non_function_root(self, node: AstNonFunctionRoot, result: List[str]): result.append(source) if node.commands: command = node.commands[0] + + if isinstance(command, AstError): + return None + name = command.identifier.partition(":")[0] d = Diagnostic( "warn", f'Ignored top-level "{name}" command outside function.' diff --git a/packages/mecha/src/mecha/api.py b/packages/mecha/src/mecha/api.py index b594f410c..6fd128a63 100644 --- a/packages/mecha/src/mecha/api.py +++ b/packages/mecha/src/mecha/api.py @@ -54,7 +54,7 @@ from pydantic import BaseModel, field_validator from tokenstream import InvalidSyntax, Preprocessor, TokenStream, set_location -from .ast import AstLiteral, AstNode, AstRoot +from .ast import AstError, AstLiteral, AstNode, AstRoot from .config import CommandTree from .database import ( CompilationDatabase, @@ -396,7 +396,7 @@ def parse( filename=str(filename) if filename else None, file=source, ) - raise DiagnosticError(DiagnosticCollection([set_location(d, exc)])) from exc + diagnostics.append(set_location(d, exc)) else: if self.cache and filename and cache_miss: try: @@ -407,6 +407,24 @@ def parse( pass return ast + if len(diagnostics) > 0: + if self.cache and filename and cache_miss: + self.cache.invalidate_changes(self.directory / filename) + + raise DiagnosticError(DiagnosticCollection(diagnostics)) + + def parse_stream( + self, + multiline: bool | None, + provide: JsonDict | None, + parser: str, + stream: TokenStream, + ): + with self.prepare_token_stream(stream, multiline=multiline): + with stream.provide(**provide or {}): + ast = delegate(parser, stream) + return ast + @overload def compile( self, diff --git a/packages/mecha/src/mecha/ast.py b/packages/mecha/src/mecha/ast.py index 075ba3c1e..a4e7e0eca 100644 --- a/packages/mecha/src/mecha/ast.py +++ b/packages/mecha/src/mecha/ast.py @@ -4,6 +4,7 @@ "AstNode", "AstChildren", "AstRoot", + "AstError", "AstCommand", "AstCommandSentinel", "AstString", @@ -119,7 +120,9 @@ from nbtlib import Byte, ByteArray, Compound, CompoundMatch, Double, Int, IntArray from nbtlib import List as ListTag from nbtlib import ListIndex, LongArray, NamedKey, Numeric, Path, String -from tokenstream import UNKNOWN_LOCATION, SourceLocation, set_location +from tokenstream import UNKNOWN_LOCATION, InvalidSyntax, SourceLocation, set_location + +from mecha.error import MechaError from .utils import string_to_number @@ -266,11 +269,18 @@ class AstChildren(AbstractChildren[AstNodeType]): class AstRoot(AstNode): """Ast root node.""" - commands: AstChildren["AstCommand"] = required_field() + commands: AstChildren["AstCommand|AstError"] = required_field() parser = "root" +@dataclass(frozen=True, slots=True) +class AstError(AstNode): + """Ast error node.""" + + error: InvalidSyntax = required_field() + + @dataclass(frozen=True, slots=True) class AstCommand(AstNode): """Ast command node.""" diff --git a/packages/mecha/src/mecha/contrib/bake_macros.py b/packages/mecha/src/mecha/contrib/bake_macros.py index c2c9f6db7..a5eeac705 100644 --- a/packages/mecha/src/mecha/contrib/bake_macros.py +++ b/packages/mecha/src/mecha/contrib/bake_macros.py @@ -31,6 +31,7 @@ from mecha import ( AstChildren, AstCommand, + AstError, AstMacroLine, AstMacroLineText, AstMacroLineVariable, @@ -135,34 +136,39 @@ class MacroBaker(Visitor): def bake_macros(self, node: AstRoot): invocation = self.invocations.get(self.database.current) - result: List[AstCommand] = [] + result: List[AstCommand|AstError] = [] modified = False - for command in node.commands: - if isinstance(command, AstMacroLine) and invocation: - baked_macro_line = self.bake_macro_line(command, invocation) - modified |= baked_macro_line is not command - command = baked_macro_line - - args = command.arguments - stack: List[AstCommand] = [command] - - while args and isinstance(subcommand := args[-1], AstCommand): - stack.append(subcommand) - args = subcommand.arguments - - last = stack[-1] - - if last.identifier == "function:name:arguments": - last = self.bake_macro_invocation(last) - if last is not stack.pop(): - for prefix in reversed(stack): - args = AstChildren([*prefix.arguments[:-1], last]) - last = replace(prefix, arguments=args) - modified = True - command = last - - result.append(command) + for c in node.commands: + match c: + case AstError() as error: + result.append(error) + continue + case AstCommand() as command: + if isinstance(command, AstMacroLine) and invocation: + baked_macro_line = self.bake_macro_line(command, invocation) + modified |= baked_macro_line is not command + command = baked_macro_line + + args = command.arguments + stack: List[AstCommand] = [command] + + while args and isinstance(subcommand := args[-1], AstCommand): + stack.append(subcommand) + args = subcommand.arguments + + last = stack[-1] + + if last.identifier == "function:name:arguments": + last = self.bake_macro_invocation(last) + if last is not stack.pop(): + for prefix in reversed(stack): + args = AstChildren([*prefix.arguments[:-1], last]) + last = replace(prefix, arguments=args) + modified = True + command = last + + result.append(command) if invocation: invocation_compilation_unit = self.database[invocation.file_instance] diff --git a/packages/mecha/src/mecha/contrib/inline_function_tag.py b/packages/mecha/src/mecha/contrib/inline_function_tag.py index 3994d7f09..b8fc36192 100644 --- a/packages/mecha/src/mecha/contrib/inline_function_tag.py +++ b/packages/mecha/src/mecha/contrib/inline_function_tag.py @@ -17,6 +17,7 @@ from mecha import ( AstChildren, AstCommand, + AstError, AstResourceLocation, AstRoot, CommandTree, @@ -66,9 +67,13 @@ class InlineFunctionTagHandler(Visitor): @rule(AstRoot) def inline_function_tag(self, node: AstRoot): changed = False - commands: List[AstCommand] = [] + commands: List[AstCommand|AstError] = [] for command in node.commands: + if isinstance(command, AstError): + commands.append(command) + continue + if command.identifier == "function:tag:name" and isinstance( tag := command.arguments[0], AstResourceLocation ): diff --git a/packages/mecha/src/mecha/contrib/nested_resources.py b/packages/mecha/src/mecha/contrib/nested_resources.py index 525fbaa84..e97449bea 100644 --- a/packages/mecha/src/mecha/contrib/nested_resources.py +++ b/packages/mecha/src/mecha/contrib/nested_resources.py @@ -29,6 +29,7 @@ from mecha import ( AstChildren, AstCommand, + AstError, AstJson, AstNode, AstResourceLocation, @@ -217,9 +218,13 @@ class NestedResourcesTransformer(MutatingReducer): @rule(AstRoot) def nested_resources(self, node: AstRoot): changed = False - commands: List[AstCommand] = [] + commands: List[AstCommand|AstError] = [] for command in node.commands: + if isinstance(command, AstError): + commands.append(command) + continue + if file_type := self.nested_resource_identifiers.get(command.identifier): name, content = command.arguments diff --git a/packages/mecha/src/mecha/contrib/nesting.py b/packages/mecha/src/mecha/contrib/nesting.py index 4a63084c1..c9b3bec4c 100644 --- a/packages/mecha/src/mecha/contrib/nesting.py +++ b/packages/mecha/src/mecha/contrib/nesting.py @@ -22,6 +22,7 @@ from mecha import ( AstChildren, AstCommand, + AstError, AstResourceLocation, AstRoot, CommandTree, @@ -34,6 +35,7 @@ rule, ) from mecha.contrib.nested_location import NestedLocationResolver +from mecha.parse import parse_root_item class NestingOptions(BaseModel): @@ -81,8 +83,8 @@ def parse_nested_root(stream: TokenStream) -> AstRoot: level, command_level = stream.indentation[-2:] - commands: List[AstCommand] = [] - + errors: list[InvalidSyntax] = [] + commands: List[AstCommand|AstError] = [] with ( stream.intercept("newline"), stream.provide( @@ -91,7 +93,11 @@ def parse_nested_root(stream: TokenStream) -> AstRoot: ), ): while True: - commands.append(delegate("root_item", stream)) + + result = parse_root_item(stream, errors, colon=True) + + if result is not None: + commands.append(result) # The command parser consumes the trailing newline so we need to rewind # to be able to use "consume_line_continuation()". @@ -101,7 +107,6 @@ def parse_nested_root(stream: TokenStream) -> AstRoot: with stream.provide(multiline=True, line_indentation=level): if not consume_line_continuation(stream): break - node = AstRoot(commands=AstChildren(commands)) return set_location(node, commands[0], commands[-1]) @@ -179,6 +184,9 @@ def nesting_execute_commands(self, node: AstCommand): single_command = None for command in root.commands: + if isinstance(command, AstError): + continue + if command.compile_hints.get("skip_execute_inline_single_command"): continue if single_command is None: @@ -270,12 +278,20 @@ def nesting_schedule_function(self, node: AstCommand): @rule(AstRoot) def nesting(self, node: AstRoot): changed = False - commands: List[AstCommand] = [] + commands: List[AstCommand|AstError] = [] for command in node.commands: + if isinstance(command, AstError): + commands.append(command) + continue + if command.identifier in self.identifier_map: result = yield from self.handle_function(command, top_level=True) commands.extend(result) + + if len(result) == 1 and result[0] is command: + continue + changed = True continue @@ -294,6 +310,10 @@ def nesting(self, node: AstRoot): if expand: changed = True for nested_command in cast(AstRoot, expand.arguments[0]).commands: + if isinstance(nested_command, AstError): + commands.append(nested_command) + continue + if nested_command.identifier == "execute:subcommand": expansion = cast(AstCommand, nested_command.arguments[0]) else: @@ -321,7 +341,7 @@ def handle_function( self, node: AstCommand, top_level: bool = False, - ) -> Generator[Diagnostic, None, List[AstCommand]]: + ) -> Generator[Diagnostic, None, List[AstCommand|AstError]]: name, *args, root = node.arguments if isinstance(name, AstResourceLocation) and isinstance(root, AstRoot): diff --git a/packages/mecha/src/mecha/contrib/statistics.py b/packages/mecha/src/mecha/contrib/statistics.py index e379534cb..2fe19badd 100644 --- a/packages/mecha/src/mecha/contrib/statistics.py +++ b/packages/mecha/src/mecha/contrib/statistics.py @@ -19,6 +19,7 @@ from mecha import ( AstCommand, AstCommandSentinel, + AstError, AstMacroLine, AstMacroLineText, AstMacroLineVariable, @@ -110,9 +111,9 @@ def root(self, node: AstRoot): self.stats.function_count += 1 for command in node.commands: - if isinstance(command, AstCommandSentinel): + if isinstance(command, (AstCommandSentinel, AstError)): continue - + behind_execute = False while command.identifier.startswith("execute"): diff --git a/packages/mecha/src/mecha/parse.py b/packages/mecha/src/mecha/parse.py index 13292fea2..c0c846dfc 100644 --- a/packages/mecha/src/mecha/parse.py +++ b/packages/mecha/src/mecha/parse.py @@ -91,7 +91,15 @@ from beet import LATEST_MINECRAFT_VERSION from beet.core.utils import VersionNumber, split_version from nbtlib import Byte, Double, Float, Int, Long, OutOfRange, Short, String -from tokenstream import InvalidSyntax, SourceLocation, TokenStream, set_location +from tokenstream import ( + InvalidSyntax, + SourceLocation, + SyntaxRules, + TokenStream, + UnexpectedToken, + set_location, + Token, +) from .ast import ( AstAdvancementPredicate, @@ -108,6 +116,7 @@ AstDustColorTransitionParticleParameters, AstDustParticleParameters, AstEntityAnchor, + AstError, AstFallingDustParticleParameters, AstGamemode, AstGreedy, @@ -638,6 +647,13 @@ def __init__(self, parser: str): self.parser = parser +class InvalidSyntaxCollection(MechaError): + errors: list[InvalidSyntax] + + def __init__(self, errors: list[InvalidSyntax]): + self.errors = errors + + @overload def delegate(parser: str) -> Parser: ... @@ -681,6 +697,7 @@ def consume_line_continuation(stream: TokenStream) -> bool: return False +# TODO: Attempt to move error recovery into AstCommand's parser def parse_root(stream: TokenStream) -> AstRoot: """Parse root.""" start = stream.peek() @@ -689,7 +706,8 @@ def parse_root(stream: TokenStream) -> AstRoot: node = AstRoot(commands=AstChildren[AstCommand]()) return set_location(node, SourceLocation(0, 1, 1)) - commands: List[AstCommand] = [] + errors: List[InvalidSyntax] = [] + commands: List[AstCommand | AstError] = [] with stream.ignore("comment"): for _ in stream.peek_until(): @@ -697,12 +715,50 @@ def parse_root(stream: TokenStream) -> AstRoot: continue if stream.get("eof"): break - commands.append(delegate("root_item", stream)) - node = AstRoot(commands=AstChildren(commands)) - return set_location(node, start, stream.current) + result = parse_root_item(stream, errors) + if result is not None: + commands.append(result) + + children = AstChildren(commands) + + node = AstRoot(commands=children) + + if stream.index < 0: + end_location = SourceLocation(1, 0, 0) + else: + end_location = stream.current.end_location + return set_location(node, start, end_location) +def consume_error(stream: TokenStream, errors: list[InvalidSyntax]): + next = stream.peek() + with stream.syntax(unknown=r"."): + while next := stream.peek(): + stream.expect() + if next.value == "\n": + break + node = AstError(errors[-1].location, errors[-1].end_location, errors[-1]) + return node + + + +def parse_root_item(stream: TokenStream, errors: list[InvalidSyntax], colon: bool = False): + + with stream.checkpoint() as commit: + try: + command: AstCommand = delegate("root_item", stream) + commit() + return command + except InvalidSyntax as exc: + errors.append(exc) + + if commit.rollback: + if colon: + with stream.syntax(colon=r":"): + return consume_error(stream, errors) + return consume_error(stream, errors) + def parse_command(stream: TokenStream) -> AstCommand: """Parse command.""" spec = get_stream_spec(stream) diff --git a/packages/mecha/src/mecha/serialize.py b/packages/mecha/src/mecha/serialize.py index 912e61ceb..b2ab7bb13 100644 --- a/packages/mecha/src/mecha/serialize.py +++ b/packages/mecha/src/mecha/serialize.py @@ -19,6 +19,7 @@ AstChildren, AstCommand, AstCoordinate, + AstError, AstItemComponent, AstItemPredicate, AstItemPredicateAlternatives, @@ -184,6 +185,10 @@ def root(self, node: AstRoot, result: List[str]): if fill := source[pos:]: result.append(self.regex_comments.sub(r"\1", fill)) + @rule(AstError) + def error(self, _node: AstError, _result: List[str]): + pass + @rule(AstCommand) def command(self, node: AstCommand, result: List[str]): prototype = self.spec.prototypes[node.identifier] diff --git a/packages/mecha/src/mecha/utils.py b/packages/mecha/src/mecha/utils.py index 72ef53cc2..e605c2c2d 100644 --- a/packages/mecha/src/mecha/utils.py +++ b/packages/mecha/src/mecha/utils.py @@ -18,7 +18,6 @@ from tokenstream import InvalidSyntax, SourceLocation, Token, set_location from .error import MechaError - ESCAPE_REGEX = re.compile(r"\\.") UNICODE_ESCAPE_REGEX = re.compile(r"\\(?:u([0-9a-fA-F]{4})|.)") AVOID_QUOTES_REGEX = re.compile(r"^[0-9A-Za-z_\.\+\-]+$") diff --git a/packages/mecha/tests/resources/multiple_errors.mcfunction b/packages/mecha/tests/resources/multiple_errors.mcfunction new file mode 100644 index 000000000..6ef0bdffa --- /dev/null +++ b/packages/mecha/tests/resources/multiple_errors.mcfunction @@ -0,0 +1,13 @@ +execute + if + entity @ + run tp ~ ~ ~ + +execute unless block ~ ~ ~ stone + if block ~ ~1 ~ air + run invalid + +execute if predicate foo:bar run say valid + +summon ~ ~ ~ {} +data modify storage foo:bar baz set {} diff --git a/packages/mecha/tests/test_parse.py b/packages/mecha/tests/test_parse.py index 972baa65a..f5146a283 100644 --- a/packages/mecha/tests/test_parse.py +++ b/packages/mecha/tests/test_parse.py @@ -5,6 +5,7 @@ from mecha import DiagnosticError, Mecha +MULTIPLE_ERRORS = Function(source_path="tests/resources/multiple_errors.mcfunction") COMMAND_EXAMPLES = Function(source_path="tests/resources/command_examples.mcfunction") MULTILINE_COMMAND_EXAMPLES = Function( source_path="tests/resources/multiline_command_examples.mcfunction" @@ -127,3 +128,14 @@ def test_player_name_no_length_restriction(mc: Mecha): ) == "scoreboard players set some_really_long_name_right_here foo 42\n" ) + + +def test_multiple_parse_errors(mc: Mecha): + with pytest.raises(DiagnosticError) as exc_info: + mc.parse(MULTIPLE_ERRORS, multiline=True) + + assert len(exc_info.value.diagnostics.exceptions) == 5 + + +if __name__ == "__main__": + Mecha().parse(MULTIPLE_ERRORS, multiline=True)