StrictDoc Documentation
strictdoc/backend/sdoc_source_code/processors/general_language_marker_processors.py
Source file coverage
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: ERA001
107
        example="""
108
# [REQ-001]
109
Content...
110
# [/REQ-001]
111
        """.lstrip(),
112
        # @relation(skip, scope=range_end)  # noqa: ERA001
113
        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: ERA001
129
        example="""
130
# [REQ-001]
131
Content...
132
# [/REQ-001]
133
        """.lstrip(),
134
        # @relation(skip, scope=range_end)  # noqa: ERA001
135
        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
        return
187
 
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
        return
194
 
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
        return
209
 
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
        return
216
 
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 parsing
263
    # 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
        return
280
 
281
    has_previous_markers = len(parse_context.markers) > 0
282
    is_consecutive = (
283
        has_previous_markers
284
        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)