Coverage for lobster/tools/trlc/converter.py: 87%

87 statements  

« 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/>. 

17 

18from typing import Iterable, List, Optional 

19from trlc import ast 

20 

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) 

40 

41 

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()) 

57 

58 if orphan_rules: 58 ↛ 59line 58 didn't jump to line 59 because the condition on line 58 was never true

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 ) 

64 

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" 

92 

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 ) 

99 

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 

104 

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 ) 

109 

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 ) 

119 

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 ) 

125 

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 ) 

135 

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: 

146 value_list.extend(self._generate_text(item_wrapper, just_field)) 

147 return rv 

148 

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 

151 

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): 

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)] 

172 

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) 

177 

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) 

191 

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. 

198 

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. 

201 

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. 

207 

208 If a field is a Array_Type, then all individual values are joined 

209 with a comma. 

210 

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) 

216 

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 ) 

222 

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 

229 

230 return "\n\n".join( 

231 f"{field}: {text}" 

232 for field, text in field_text_map.items() 

233 )