Coverage for src/m6rclib/metaphor_formatter.py: 95%

1# Copyright 2024 M6R Ltd.

2#

3# Licensed under the Apache License, Version 2.0 (the "License");

4# you may not use this file except in compliance with the License.

5# You may obtain a copy of the License at

6#

7# http://www.apache.org/licenses/LICENSE-2.0

8#

9# Unless required by applicable law or agreed to in writing, software

10# distributed under the License is distributed on an "AS IS" BASIS,

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

12# See the License for the specific language governing permissions and

13# limitations under the License.

14

15"""Functions for formatting Metaphor AST nodes and error messages."""

16

17 import io

18 from typing import List, TextIO

19

20 from .metaphor_ast_node import MetaphorASTNode, MetaphorASTNodeType

21 from .metaphor_parser import MetaphorParserSyntaxError

22

23

24 def format_ast(node: MetaphorASTNode) -> str:

25 """Format an AST node and its children as a string.

26

27 Args:

28 node: The root node to format

29

30 Returns:

31 Formatted string representation of the AST

32 """

33 output = io.StringIO()

34 _format_node(node, 0, output)

35 return output.getvalue()

36

37

38 def _format_node(node: MetaphorASTNode, depth: int, out: TextIO) -> None:

39 """Recursively format a node and its children.

40

41 Args:

42 node: Current node being processed

43 depth: Current tree depth

44 out: Output buffer to write to

45 """

46 if node.node_type != MetaphorASTNodeType.ROOT:

47 indent = " " * ((depth - 1) * 4)

48 if node.node_type == MetaphorASTNodeType.TEXT:

49 out.write(f"{indent}{node.value}\n")

50 return

51

52 # Map node types to keywords

53 node_type_map = {

54 MetaphorASTNodeType.ACTION: "Action:",

55 MetaphorASTNodeType.CONTEXT: "Context:",

56 MetaphorASTNodeType.ROLE: "Role:"

57 }

58 keyword = node_type_map.get(node.node_type, "")

59

60 if keyword: 60 ↛ 66line 60 didn't jump to line 66 because the condition on line 60 was always true

61 out.write(f"{indent}{keyword}")

62 if node.value: 62 ↛ 64line 62 didn't jump to line 64 because the condition on line 62 was always true

63 out.write(f" {node.value}")

64 out.write("\n")

65

66 for child in node.children:

67 _format_node(child, depth + 1, out)

68

69

70 def format_errors(errors: List[MetaphorParserSyntaxError]) -> str:

71 """Format a list of syntax errors as a string.

72

73 Args:

74 errors: List of syntax errors to format

75

76 Returns:

77 Formatted error string with each error on separate lines

78 """

79 output = io.StringIO()

80

81 for error in errors:

82 caret = " " * (error.column - 1)

83 error_message = (

84 f"{error.message}: line {error.line}, column {error.column}, "

85 f"file {error.filename}\n{caret}|\n{caret}v\n{error.input_text}"

86 )

87 output.write(f"----------------\n{error_message}\n")

88

89 output.write("----------------\n")

90 return output.getvalue()