Coverage for lobster/tools/trlc/converter.py: 95%
87 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-09-22 04:43 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-09-22 04:43 +0000
1# LOBSTER - Lightweight Open BMW Software Traceability Evidence Report
2# Copyright (C) 2025-2026 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU Affero General Public License as
6# published by the Free Software Foundation, either version 3 of the
7# License, or (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12# Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public
15# License along with this program. If not, see
16# <https://www.gnu.org/licenses/>.
18from typing import Iterable, List, Optional
19from trlc import ast
21from lobster.common.items import Item, Requirement, Tracing_Tag
22from lobster.common.location import File_Reference
23from lobster.tools.trlc.conversion_rule import ConversionRule
24from lobster.tools.trlc.conversion_rule_lookup import (
25 build_record_type_to_conversion_rule_lookup,
26 get_record_types,
27)
28from lobster.tools.trlc.errors import (
29 InvalidConversionRuleError,
30 TupleComponentError,
31 TupleToStringFailedError,
32 TupleToStringMissingError,
33)
34from lobster.tools.trlc.hierarchy_tree import build_children_lookup
35from lobster.tools.trlc.item_wrapper import ItemWrapper
36from lobster.tools.trlc.text_generation import build_text_from_instructions
37from lobster.tools.trlc.to_string_rules import (
38 ToStringRules, build_tuple_type_to_ruleset_map,
39)
42class Converter:
43 def __init__(
44 self,
45 conversion_rules: Iterable[ConversionRule],
46 to_string_rules: Iterable[ToStringRules],
47 symbol_table: ast.Symbol_Table,
48 ) -> None:
49 self._conversion_rule_lookup = build_record_type_to_conversion_rule_lookup(
50 conversion_rules=conversion_rules,
51 children_lookup=build_children_lookup(symbol_table),
52 symbol_table=symbol_table,
53 )
54 # check if any rule is left-over and could not be allocated to a record type
55 orphan_rules = set(conversion_rules) - \
56 set(self._conversion_rule_lookup.values())
58 if orphan_rules:
59 raise self._build_orphan_rules_exception(symbol_table, orphan_rules)
60 self._to_string_rules = build_tuple_type_to_ruleset_map(
61 symbol_table=symbol_table,
62 to_string_rule_sets=to_string_rules,
63 )
65 def _build_orphan_rules_exception(
66 self,
67 symbol_table: ast.Symbol_Table,
68 orphan_rules: Iterable[ConversionRule],
69 ) -> InvalidConversionRuleError:
70 orphan_rule_names = ", ".join(
71 f"{rule.package_name}.{rule.type_name}" for rule in orphan_rules
72 )
73 available_type_names = ', '.join(
74 record_type.fully_qualified_name()
75 for record_type in get_record_types(symbol_table)
76 )
77 if available_type_names:
78 available_types_message = (
79 f"Detected record types are '{available_type_names}'."
80 )
81 else:
82 available_types_message = (
83 "The TRLC symbol table contains no record types at all."
84 )
85 if self._conversion_rule_lookup:
86 successfully_mapped_types = ', '.join(
87 f"{rule.package_name}.{rule.type_name}"
88 for rule in set(self._conversion_rule_lookup.values())
89 )
90 else:
91 successfully_mapped_types = "none"
93 return InvalidConversionRuleError(
94 f"The following conversion rules do not match any record type in "
95 f"the TRLC symbol table: {orphan_rule_names}. {available_types_message} "
96 f"The following conversion rules were successfully mapped to TRLC types: "
97 f"{successfully_mapped_types}."
98 )
100 def generate_lobster_object(self, n_obj: ast.Record_Object) -> Optional[Item]:
101 rule = self._conversion_rule_lookup.get(n_obj.n_typ)
102 if not rule:
103 return None
105 if rule.lobster_namespace != "req": 105 ↛ 106line 105 didn't jump to line 106 because the condition on line 105 was never true
106 raise NotImplementedError(
107 f"Conversion for namespace '{rule.lobster_namespace}' not implemented."
108 )
110 item_wrapper = ItemWrapper(n_obj)
111 item_tag = Tracing_Tag(
112 namespace=rule.lobster_namespace,
113 tag=n_obj.fully_qualified_name(),
114 version=(
115 item_wrapper.get_field_value_or_none(rule.version)
116 if rule.version
117 else None),
118 )
120 item_loc = File_Reference(
121 filename=n_obj.location.file_name,
122 line=n_obj.location.line_no,
123 column=n_obj.location.col_no
124 )
126 item_text = self._get_description(item_wrapper, rule.description_fields)
127 rv = Requirement(
128 tag=item_tag,
129 location=item_loc,
130 framework="TRLC",
131 kind=n_obj.n_typ.name,
132 name=n_obj.fully_qualified_name(),
133 text=item_text
134 )
136 for tag_entry in rule.tags:
137 for field_str_value in self._generate_text(item_wrapper, tag_entry.field):
138 tag = Tracing_Tag.from_text(tag_entry.namespace, field_str_value)
139 rv.add_tracing_target(tag)
140 for value_list, fields in (
141 (rv.just_up, rule.justification_up_fields),
142 (rv.just_down, rule.justification_down_fields),
143 (rv.just_global, rule.justification_global_fields),
144 ):
145 for just_field in fields: 145 ↛ 146line 145 didn't jump to line 146 because the loop on line 145 never started
146 value_list.extend(self._generate_text(item_wrapper, just_field))
147 return rv
149 def _generate_text(self, item_wrapper: ItemWrapper, field_name: str) -> List[str]:
150 """Generates a list of texts for the values in the given field
152 The function always returns a list of strings, even if the field is a
153 single-value field.
154 """
155 field_value = item_wrapper.get_field(field_name)
156 if field_value is None:
157 return []
158 raw_field = item_wrapper.get_field_raw(field_name)
159 if isinstance(raw_field.typ, ast.Array_Type):
160 texts = []
161 for element in raw_field.value:
162 if isinstance(element, ast.Tuple_Aggregate): 162 ↛ 163line 162 didn't jump to line 163 because the condition on line 162 was never true
163 texts.append(self._tuple_value_as_string(element))
164 elif isinstance(element, ast.Record_Reference):
165 texts.append(element.target.fully_qualified_name())
166 else:
167 texts.append(str(element.to_python_object()))
168 return texts
169 if isinstance(raw_field, ast.Tuple_Aggregate):
170 return [self._tuple_value_as_string(raw_field)]
171 return [str(field_value)]
173 def _tuple_value_as_string(self, tuple_aggregate: ast.Tuple_Aggregate):
174 to_string_rules = self._to_string_rules.get(tuple_aggregate.typ)
175 if not to_string_rules:
176 raise TupleToStringMissingError(tuple_aggregate)
178 # We have functions, so we attempt to apply until we get
179 # one that works, in order.
180 earlier_errors = []
181 for instruction_list in to_string_rules.rules:
182 try:
183 return build_text_from_instructions(instruction_list, tuple_aggregate)
184 except TupleComponentError as e:
185 # If the instruction set is invalid, we skip to the next one.
186 earlier_errors.append(e)
187 continue
188 # If we reach here, it means no instruction worked.
189 # We raise an error to indicate that no valid instruction set was found.
190 raise TupleToStringFailedError(tuple_aggregate, earlier_errors)
192 def _get_description(
193 self,
194 item_wrapper: ItemWrapper,
195 description_fields: List[str],
196 ) -> str:
197 """Generates a description text for the LOBSTER item.
199 The text of a LOBSTER item is always a single string, not a list of strings,
200 even if there are multiple description fields to consider.
202 This string uses a different format depending on the number of description
203 fields:
204 - If there is only one description field, it returns the text of that field.
205 - If there are multiple description fields, it formats them as "field: text"
206 pairs and joins them with two newlines.
208 If a field is a Array_Type, then all individual values are joined
209 with a comma.
211 If a field is a Tuple_Aggregate, it is converted to a string using the
212 `self._tuple_value_as_string` method.
213 """
214 def join_field_str_values(texts: Iterable[str]) -> str:
215 return ', '.join(texts)
217 # if there is only one description field, then return it directly
218 if len(description_fields) == 1:
219 return join_field_str_values(
220 self._generate_text(item_wrapper, description_fields[0]),
221 )
223 # if there are multiple description fields (or zero), then format them
224 field_text_map = {}
225 for field in description_fields:
226 text = join_field_str_values(self._generate_text(item_wrapper, field))
227 if text:
228 field_text_map[field] = text
230 return "\n\n".join(
231 f"{field}: {text}"
232 for field, text in field_text_map.items()
233 )