Path:
strictdoc/backend/sdoc_source_code/processors/general_language_marker_processors.py
Lines:
308
Non-empty lines:
267
Non-empty lines covered with requirements:
267 / 267 (100.0%)
Functions:
9
Functions covered by requirements:
9 / 9 (100.0%)
1
"""2
@relation(SDOC-SRS-33, scope=file)3
"""4
5
from typing import List, Optional, Tuple, Union
6
7
from textx import get_location
8
9
from strictdoc.backend.sdoc.error_handling import StrictDocSemanticError
10
from strictdoc.backend.sdoc_source_code.models.language_item_marker import (
11
LanguageItemMarker,
12
)13
from strictdoc.backend.sdoc_source_code.models.line_marker import LineMarker
14
from strictdoc.backend.sdoc_source_code.models.range_marker import (
15
RangeMarker,
16
)17
from strictdoc.backend.sdoc_source_code.models.source_file_info import (
18
SourceFileTraceabilityInfo,
19
)20
from strictdoc.backend.sdoc_source_code.parse_context import ParseContext
21
from strictdoc.helpers.cast import assert_cast
22
from strictdoc.helpers.list import find_duplicates
23
24
25
def validate_marker_uids(
26
marker: Union[LanguageItemMarker, LineMarker, RangeMarker],
27
parse_context: ParseContext,
28
) -> None:
29
possible_duplicates = find_duplicates(marker.reqs)
30
if len(possible_duplicates) > 0:
31
location = get_location(marker)
32
33
raise ValueError(
34
"@relation marker contains duplicate node UIDs: "35
f"{possible_duplicates}. Location: {parse_context.filename}:{location['line']}."
36
)37
38
39
def _handle_skip_marker(
40
marker: Union[RangeMarker, LanguageItemMarker, LineMarker],
41
parse_context: ParseContext,
42
) -> None:
43
assert marker.ng_is_nodoc, marker
44
assert marker.ng_source_line_begin is not None, marker
45
assert marker.ng_source_column_begin is not None, marker
46
47
if marker.is_begin():
48
parse_context.marker_stack.append(marker)
49
elif marker.is_end():
50
try:
51
current_top_marker = parse_context.marker_stack.pop()
52
if (
53
not current_top_marker.ng_is_nodoc
54
or current_top_marker.is_end()
55
):56
raise create_begin_end_range_reqs_mismatch_error(
57
parse_context.filename,
58
assert_cast(current_top_marker.ng_source_line_begin, int),
59
assert_cast(current_top_marker.ng_source_column_begin, int),
60
current_top_marker.reqs,
61
marker.reqs,
62
)63
except IndexError:
64
raise create_end_without_begin_error(
65
parse_context.filename,
66
marker.ng_source_line_begin,
67
marker.ng_source_column_begin,
68
) from None
69
70
71
def source_file_traceability_info_processor(
72
source_file_traceability_info: SourceFileTraceabilityInfo,
73
parse_context: ParseContext,
74
) -> None:
75
if len(parse_context.marker_stack) > 0:
76
if any(
77
not marker_.ng_is_nodoc for marker_ in parse_context.marker_stack
78
):79
raise create_unmatch_range_error(
80
parse_context.marker_stack, filename=parse_context.filename
81
)82
source_file_traceability_info.markers = parse_context.markers
83
source_file_traceability_info.file_stats = parse_context.file_stats
84
source_file_traceability_info.ng_map_reqs_to_markers = (
85
parse_context.map_reqs_to_markers
86
)87
88
89
def create_begin_end_range_reqs_mismatch_error(
90
filename: str,
91
line: int,
92
col: int,
93
lhs_marker_reqs: List[str],
94
rhs_marker_reqs: List[str],
95
) -> StrictDocSemanticError:
96
lhs_marker_reqs_str = ", ".join(lhs_marker_reqs)
97
rhs_marker_reqs_str = ", ".join(rhs_marker_reqs)
98
99
return StrictDocSemanticError(
100
title="STRICTDOC RANGE: BEGIN and END requirements mismatch",
101
hint=(
102
"STRICT RANGE marker should START and END "103
"with the same requirement(s): "104
f"'{lhs_marker_reqs_str}' != '{rhs_marker_reqs_str}'."
105
),106
# @relation(skip, scope=range_start) # noqa: ERA001107
example="""
108
# [REQ-001]109
Content...110
# [/REQ-001]111
""".lstrip(),
112
# @relation(skip, scope=range_end) # noqa: ERA001113
line=line,
114
col=col,
115
filename=filename,
116
)117
118
119
def create_end_without_begin_error(
120
filename: str, line: int, col: int
121
) -> StrictDocSemanticError:
122
return StrictDocSemanticError(
123
title="STRICTDOC RANGE: END marker without preceding BEGIN marker",
124
hint=(
125
"STRICT RANGE shall be opened with "126
"START marker and ended with END marker."127
),128
# @relation(skip, scope=range_start) # noqa: ERA001129
example="""
130
# [REQ-001]131
Content...132
# [/REQ-001]133
""".lstrip(),
134
# @relation(skip, scope=range_end) # noqa: ERA001135
line=line,
136
col=col,
137
filename=filename,
138
)139
140
141
def create_unmatch_range_error(
142
unmatched_ranges: List[Union[RangeMarker, LanguageItemMarker, LineMarker]],
143
filename: Optional[str],
144
) -> StrictDocSemanticError:
145
assert isinstance(unmatched_ranges, list)
146
assert len(unmatched_ranges) > 0
147
range_locations: List[Tuple[int, int]] = []
148
for unmatched_range_ in unmatched_ranges:
149
assert unmatched_range_.ng_source_line_begin is not None
150
assert unmatched_range_.ng_source_column_begin is not None
151
range_locations.append(
152
(153
unmatched_range_.ng_source_line_begin,
154
unmatched_range_.ng_source_column_begin,
155
)156
)157
first_location = range_locations[0]
158
hint: Optional[str] = None
159
if len(unmatched_ranges) > 1:
160
range_lines = range_locations[1:]
161
hint = f"The @relation keywords are also unmatched on lines: {range_lines}."
162
163
return StrictDocSemanticError(
164
title="Unmatched @relation keyword found in source file.",
165
hint=hint,
166
# @relation(skip, scope=range_start)167
example=(
168
"Each @relation keyword must be matched with a closing keyword. "169
"Example:\n"
170
"@relation(REQ-001, scope=range_start)\n"
171
"...\n"
172
"@relation(REQ-001, scope=range_end)"173
),174
# @relation(skip, scope=range_end)175
line=first_location[0],
176
col=first_location[1],
177
filename=filename,
178
)179
180
181
def language_item_marker_processor(
182
marker: LanguageItemMarker, parse_context: ParseContext
183
) -> None:
184
if marker.ng_is_nodoc:
185
_handle_skip_marker(marker, parse_context)
186
return187
188
if (
189
len(parse_context.marker_stack) > 0
190
and parse_context.marker_stack[-1].ng_is_nodoc
191
):192
# This marker is within a "@relation(skip...)" block, so we ignore it.193
return194
195
parse_context.markers.append(marker)
196
197
assert marker.ng_source_line_begin is not None
198
for req in marker.reqs:
199
markers = parse_context.map_reqs_to_markers.setdefault(req, [])
200
markers.append(marker)
201
202
203
def range_marker_processor(
204
marker: RangeMarker, parse_context: ParseContext
205
) -> None:
206
if marker.ng_is_nodoc:
207
_handle_skip_marker(marker, parse_context)
208
return209
210
if (
211
len(parse_context.marker_stack) > 0
212
and parse_context.marker_stack[-1].ng_is_nodoc
213
):214
# This marker is within a "@relation(skip...)" block, so we ignore it.215
return216
217
parse_context.markers.append(marker)
218
219
assert marker.ng_source_line_begin is not None
220
221
if marker.is_begin():
222
marker.ng_range_line_begin = marker.ng_source_line_begin
223
parse_context.marker_stack.append(marker)
224
assert marker.ng_source_line_begin is not None
225
for req in marker.reqs:
226
markers = parse_context.map_reqs_to_markers.setdefault(req, [])
227
markers.append(marker)
228
229
elif marker.is_end():
230
try:
231
current_top_marker = parse_context.marker_stack.pop()
232
if marker.reqs != current_top_marker.reqs:
233
assert marker.ng_source_line_begin is not None
234
raise create_begin_end_range_reqs_mismatch_error(
235
parse_context.filename,
236
assert_cast(marker.ng_source_line_begin, int),
237
assert_cast(marker.ng_source_column_begin, int),
238
current_top_marker.reqs,
239
marker.reqs,
240
)241
242
current_top_marker.ng_range_line_end = marker.ng_source_line_begin
243
244
marker.ng_range_line_end = marker.ng_range_line_begin
245
marker.ng_range_line_begin = current_top_marker.ng_range_line_begin
246
247
except IndexError:
248
raise create_end_without_begin_error(
249
parse_context.filename,
250
assert_cast(marker.ng_source_line_begin, int),
251
assert_cast(marker.ng_source_column_begin, int),
252
) from None
253
else:
254
raise NotImplementedError
255
256
257
def line_marker_processor(
258
line_marker: LineMarker, parse_context: ParseContext
259
) -> None:
260
validate_marker_uids(line_marker, parse_context)
261
262
# If the object is coming from textX, obtain information from textX parsing263
# information.264
line: int
265
if line_marker.ng_source_line_begin is None:
266
location = get_location(line_marker)
267
line = location["line"]
268
line_marker.ng_source_line_begin = line
269
line_marker.ng_range_line_begin = line
270
line_marker.ng_range_line_end = line + 1
271
else:
272
line = line_marker.ng_source_line_begin
273
274
if (
275
len(parse_context.marker_stack) > 0
276
and parse_context.marker_stack[-1].ng_is_nodoc
277
):278
# This marker is within a "@relation(skip...)" block, so we ignore it.279
return280
281
has_previous_markers = len(parse_context.markers) > 0
282
is_consecutive = (
283
has_previous_markers284
and parse_context.markers[-1].ng_range_line_end == line
285
)286
if is_consecutive:
287
raise StrictDocSemanticError(
288
title="Consecutive LineMarkers are not allowed",
289
hint=None,
290
example=None,
291
line=line,
292
filename=parse_context.filename,
293
)294
295
is_at_eof = line == parse_context.file_stats.lines_total
296
if is_at_eof:
297
raise StrictDocSemanticError(
298
title="LineMarker cannot be followed by EOF",
299
hint=None,
300
example=None,
301
line=line,
302
filename=parse_context.filename,
303
)304
305
parse_context.markers.append(line_marker)
306
for req in line_marker.reqs:
307
markers = parse_context.map_reqs_to_markers.setdefault(req, [])
308
markers.append(line_marker)