Coverage for lobster/tools/trlc/converter.py: 95%
87 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-09-30 04:59 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-09-30 04:59 +0000
1from typing import Iterable, List, Optional
2from trlc import ast
4from lobster.common.items import Item, Requirement, Tracing_Tag
5from lobster.common.location import File_Reference
6from lobster.tools.trlc.conversion_rule import ConversionRule
7from lobster.tools.trlc.conversion_rule_lookup import (
8 build_record_type_to_conversion_rule_lookup,
9 get_record_types,
10)
11from lobster.tools.trlc.errors import (
12 InvalidConversionRuleError,
13 TupleComponentError,
14 TupleToStringFailedError,
15 TupleToStringMissingError,
16)
17from lobster.tools.trlc.hierarchy_tree import build_children_lookup
18from lobster.tools.trlc.item_wrapper import ItemWrapper
19from lobster.tools.trlc.text_generation import build_text_from_instructions
20from lobster.tools.trlc.to_string_rules import (
21 ToStringRules, build_tuple_type_to_ruleset_map,
22)
25class Converter:
26 def __init__(
27 self,
28 conversion_rules: Iterable[ConversionRule],
29 to_string_rules: Iterable[ToStringRules],
30 symbol_table: ast.Symbol_Table,
31 ) -> None:
32 self._conversion_rule_lookup = build_record_type_to_conversion_rule_lookup(
33 conversion_rules=conversion_rules,
34 children_lookup=build_children_lookup(symbol_table),
35 symbol_table=symbol_table,
36 )
37 # check if any rule is left-over and could not be allocated to a record type
38 orphan_rules = set(conversion_rules) - \
39 set(self._conversion_rule_lookup.values())
41 if orphan_rules:
42 raise self._build_orphan_rules_exception(symbol_table, orphan_rules)
43 self._to_string_rules = build_tuple_type_to_ruleset_map(
44 symbol_table=symbol_table,
45 to_string_rule_sets=to_string_rules,
46 )
48 def _build_orphan_rules_exception(
49 self,
50 symbol_table: ast.Symbol_Table,
51 orphan_rules: Iterable[ConversionRule],
52 ) -> InvalidConversionRuleError:
53 orphan_rule_names = ", ".join(
54 f"{rule.package_name}.{rule.type_name}" for rule in orphan_rules
55 )
56 available_type_names = ', '.join(
57 record_type.fully_qualified_name()
58 for record_type in get_record_types(symbol_table)
59 )
60 if available_type_names:
61 available_types_message = (
62 f"Detected record types are '{available_type_names}'."
63 )
64 else:
65 available_types_message = (
66 "The TRLC symbol table contains no record types at all."
67 )
68 if self._conversion_rule_lookup:
69 successfully_mapped_types = ', '.join(
70 f"{rule.package_name}.{rule.type_name}"
71 for rule in set(self._conversion_rule_lookup.values())
72 )
73 else:
74 successfully_mapped_types = "none"
76 return InvalidConversionRuleError(
77 f"The following conversion rules do not match any record type in "
78 f"the TRLC symbol table: {orphan_rule_names}. {available_types_message} "
79 f"The following conversion rules were successfully mapped to TRLC types: "
80 f"{successfully_mapped_types}."
81 )
83 def generate_lobster_object(self, n_obj: ast.Record_Object) -> Optional[Item]:
84 rule = self._conversion_rule_lookup.get(n_obj.n_typ)
85 if not rule:
86 return None
88 if rule.lobster_namespace != "req": 88 ↛ 89line 88 didn't jump to line 89 because the condition on line 88 was never true
89 raise NotImplementedError(
90 f"Conversion for namespace '{rule.lobster_namespace}' not implemented."
91 )
93 item_wrapper = ItemWrapper(n_obj)
94 item_tag = Tracing_Tag(
95 namespace=rule.lobster_namespace,
96 tag=n_obj.fully_qualified_name(),
97 version=(
98 item_wrapper.get_field_value_or_none(rule.version)
99 if rule.version
100 else None),
101 )
103 item_loc = File_Reference(
104 filename=n_obj.location.file_name,
105 line=n_obj.location.line_no,
106 column=n_obj.location.col_no
107 )
109 item_text = self._get_description(item_wrapper, rule.description_fields)
110 rv = Requirement(
111 tag=item_tag,
112 location=item_loc,
113 framework="TRLC",
114 kind=n_obj.n_typ.name,
115 name=n_obj.fully_qualified_name(),
116 text=item_text
117 )
119 for tag_entry in rule.tags:
120 for field_str_value in self._generate_text(item_wrapper, tag_entry.field):
121 tag = Tracing_Tag.from_text(tag_entry.namespace, field_str_value)
122 rv.add_tracing_target(tag)
123 for value_list, fields in (
124 (rv.just_up, rule.justification_up_fields),
125 (rv.just_down, rule.justification_down_fields),
126 (rv.just_global, rule.justification_global_fields),
127 ):
128 for just_field in fields: 128 ↛ 129line 128 didn't jump to line 129 because the loop on line 128 never started
129 value_list.extend(self._generate_text(item_wrapper, just_field))
130 return rv
132 def _generate_text(self, item_wrapper: ItemWrapper, field_name: str) -> List[str]:
133 """Generates a list of texts for the values in the given field
135 The function always returns a list of strings, even if the field is a
136 single-value field.
137 """
138 field_value = item_wrapper.get_field(field_name)
139 if field_value is None:
140 return []
141 raw_field = item_wrapper.get_field_raw(field_name)
142 if isinstance(raw_field.typ, ast.Array_Type):
143 texts = []
144 for element in raw_field.value:
145 if isinstance(element, ast.Tuple_Aggregate): 145 ↛ 146line 145 didn't jump to line 146 because the condition on line 145 was never true
146 texts.append(self._tuple_value_as_string(element))
147 elif isinstance(element, ast.Record_Reference):
148 texts.append(element.target.fully_qualified_name())
149 else:
150 texts.append(str(element.to_python_object()))
151 return texts
152 elif isinstance(raw_field, ast.Tuple_Aggregate):
153 return [self._tuple_value_as_string(raw_field)]
154 return [str(field_value)]
156 def _tuple_value_as_string(self, tuple_aggregate: ast.Tuple_Aggregate):
157 to_string_rules = self._to_string_rules.get(tuple_aggregate.typ)
158 if not to_string_rules:
159 raise TupleToStringMissingError(tuple_aggregate)
161 # We have functions, so we attempt to apply until we get
162 # one that works, in order.
163 earlier_errors = []
164 for instruction_list in to_string_rules.rules:
165 try:
166 return build_text_from_instructions(instruction_list, tuple_aggregate)
167 except TupleComponentError as e:
168 # If the instruction set is invalid, we skip to the next one.
169 earlier_errors.append(e)
170 continue
171 # If we reach here, it means no instruction worked.
172 # We raise an error to indicate that no valid instruction set was found.
173 raise TupleToStringFailedError(tuple_aggregate, earlier_errors)
175 def _get_description(
176 self,
177 item_wrapper: ItemWrapper,
178 description_fields: List[str],
179 ) -> str:
180 """Generates a description text for the LOBSTER item.
182 The text of a LOBSTER item is always a single string, not a list of strings,
183 even if there are multiple description fields to consider.
185 This string uses a different format depending on the number of description
186 fields:
187 - If there is only one description field, it returns the text of that field.
188 - If there are multiple description fields, it formats them as "field: text"
189 pairs and joins them with two newlines.
191 If a field is a Array_Type, then all individual values are joined
192 with a comma.
194 If a field is a Tuple_Aggregate, it is converted to a string using the
195 `self._tuple_value_as_string` method.
196 """
197 def join_field_str_values(texts: Iterable[str]) -> str:
198 return ', '.join(texts)
200 # if there is only one description field, then return it directly
201 if len(description_fields) == 1:
202 return join_field_str_values(
203 self._generate_text(item_wrapper, description_fields[0]),
204 )
206 # if there are multiple description fields (or zero), then format them
207 field_text_map = {}
208 for field in description_fields:
209 text = join_field_str_values(self._generate_text(item_wrapper, field))
210 if text:
211 field_text_map[field] = text
213 return "\n\n".join(
214 f"{field}: {text}"
215 for field, text in field_text_map.items()
216 )