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

1from typing import Iterable, List, Optional 

2from trlc import ast 

3 

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) 

23 

24 

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

40 

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 ) 

47 

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" 

75 

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 ) 

82 

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 

87 

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 ) 

92 

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 ) 

102 

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 ) 

108 

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 ) 

118 

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 

131 

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 

134 

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

155 

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) 

160 

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) 

174 

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. 

181 

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. 

184 

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. 

190 

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

192 with a comma. 

193 

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) 

199 

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 ) 

205 

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 

212 

213 return "\n\n".join( 

214 f"{field}: {text}" 

215 for field, text in field_text_map.items() 

216 )