StrictDoc Documentation
strictdoc/backend/sdoc_source_code/reader.py
Source file coverage
Path:
strictdoc/backend/sdoc_source_code/reader.py
Lines:
236
Non-empty lines:
196
Non-empty lines covered with requirements:
196 / 196 (100.0%)
Functions:
10
Functions covered by requirements:
10 / 10 (100.0%)
1
"""
2
@relation(SDOC-SRS-142, scope=file)
3
"""
4
 
5
from functools import partial
6
from typing import Optional, TypedDict
7
 
8
from textx import get_location, metamodel_from_str
9
 
10
from strictdoc.backend.sdoc_source_code.grammar import SOURCE_FILE_GRAMMAR
11
from strictdoc.backend.sdoc_source_code.models.language_item_marker import (
12
    LanguageItemMarker,
13
)
14
from strictdoc.backend.sdoc_source_code.models.line_marker import LineMarker
15
from strictdoc.backend.sdoc_source_code.models.range_marker import (
16
    RangeMarker,
17
)
18
from strictdoc.backend.sdoc_source_code.models.requirement_marker import Req
19
from strictdoc.backend.sdoc_source_code.models.source_file_info import (
20
    SourceFileTraceabilityInfo,
21
)
22
from strictdoc.backend.sdoc_source_code.parse_context import ParseContext
23
from strictdoc.backend.sdoc_source_code.processors.general_language_marker_processors import (
24
    _handle_skip_marker,
25
    create_begin_end_range_reqs_mismatch_error,
26
    create_end_without_begin_error,
27
    create_unmatch_range_error,
28
    line_marker_processor,
29
    validate_marker_uids,
30
)
31
from strictdoc.helpers.cast import assert_cast
32
from strictdoc.helpers.file_stats import SourceFileStats
33
from strictdoc.helpers.file_system import file_open_read_utf8
34
from strictdoc.helpers.textx import drop_textx_meta
35
 
36
 
37
class TextXLocation(TypedDict):
38
    line: int
39
    col: int
40
    filename: str
41
 
42
 
43
def req_processor(req: Req) -> None:
44
    assert isinstance(req, Req), (
45
        f"Expected req to be Req, got: {req}, {type(req)}"
46
    )
47
    location = get_location(req)
48
    assert location
49
    req.ng_source_line = location["line"]
50
    req.ng_source_column = location["col"]
51
 
52
 
53
def source_file_traceability_info_processor(
54
    source_file_traceability_info: SourceFileTraceabilityInfo,
55
    parse_context: ParseContext,
56
) -> None:
57
    if len(parse_context.marker_stack) > 0:
58
        if any(
59
            not marker_.ng_is_nodoc for marker_ in parse_context.marker_stack
60
        ):
61
            raise create_unmatch_range_error(
62
                parse_context.marker_stack, filename=parse_context.filename
63
            )
64
    source_file_traceability_info.markers = parse_context.markers
65
    source_file_traceability_info.file_stats = parse_context.file_stats
66
 
67
 
68
def range_marker_processor(
69
    marker: RangeMarker, parse_context: ParseContext
70
) -> None:
71
    validate_marker_uids(marker, parse_context)
72
 
73
    location = get_location(marker)
74
    line = location["line"]
75
    column = location["col"]
76
    marker.ng_source_line_begin = line
77
    marker.ng_source_column_begin = column
78
 
79
    if marker.ng_is_nodoc:
80
        _handle_skip_marker(marker, parse_context)
81
        return
82
 
83
    if (
84
        len(parse_context.marker_stack) > 0
85
        and parse_context.marker_stack[-1].ng_is_nodoc
86
    ):
87
        # This marker is within a "@relation(skip...)" block, so we ignore it.
88
        return
89
 
90
    parse_context.markers.append(marker)
91
 
92
    if marker.is_begin():
93
        marker.ng_range_line_begin = line
94
        parse_context.marker_stack.append(marker)
95
        for req in marker.reqs:
96
            markers = parse_context.map_reqs_to_markers.setdefault(req, [])
97
            markers.append(marker)
98
 
99
    elif marker.is_end():
100
        try:
101
            current_top_marker = parse_context.marker_stack.pop()
102
            if marker.reqs != current_top_marker.reqs:
103
                raise create_begin_end_range_reqs_mismatch_error(
104
                    parse_context.filename,
105
                    assert_cast(current_top_marker.ng_source_line_begin, int),
106
                    assert_cast(current_top_marker.ng_range_line_begin, int),
107
                    current_top_marker.reqs,
108
                    marker.reqs,
109
                )
110
            current_top_marker.ng_range_line_end = line
111
 
112
            marker.ng_range_line_begin = current_top_marker.ng_range_line_begin
113
            marker.ng_range_line_end = line
114
 
115
        except IndexError:
116
            raise create_end_without_begin_error(
117
                parse_context.filename, line, location["col"]
118
            ) from None
119
    else:
120
        raise NotImplementedError
121
 
122
 
123
def language_item_marker_processor(
124
    language_item_marker: LanguageItemMarker, parse_context: ParseContext
125
) -> None:
126
    validate_marker_uids(language_item_marker, parse_context)
127
 
128
    location = get_location(language_item_marker)
129
    line = location["line"]
130
    column = location["col"]
131
 
132
    language_item_marker.ng_source_line_begin = line
133
    language_item_marker.ng_source_column_begin = column
134
    language_item_marker.ng_range_line_begin = 1
135
    language_item_marker.ng_range_line_end = (
136
        parse_context.file_stats.lines_total
137
    )
138
 
139
    if language_item_marker.ng_is_nodoc:
140
        _handle_skip_marker(language_item_marker, parse_context)
141
        return
142
 
143
    if (
144
        len(parse_context.marker_stack) > 0
145
        and parse_context.marker_stack[-1].ng_is_nodoc
146
    ):
147
        # This marker is within a "@relation(skip...)" block, so we ignore it.
148
        return
149
 
150
    parse_context.markers.append(language_item_marker)
151
 
152
    # Language item markers supported by this general reader can only
153
    # be of scope=file. Only the language-aware parsing results in
154
    # markers also having scope=function or scope=class.
155
    language_item_marker.set_description("entire file")
156
 
157
    for req in language_item_marker.reqs:
158
        markers = parse_context.map_reqs_to_markers.setdefault(req, [])
159
        markers.append(language_item_marker)
160
 
161
 
162
class SourceFileTraceabilityReader:
163
    SOURCE_FILE_MODELS = [
164
        LanguageItemMarker,
165
        LineMarker,
166
        Req,
167
        SourceFileTraceabilityInfo,
168
        RangeMarker,
169
    ]
170
 
171
    @staticmethod
172
    def supported_elements() -> list[str]:
173
        return []
174
 
175
    def __init__(self) -> None:
176
        self.meta_model = metamodel_from_str(
177
            SOURCE_FILE_GRAMMAR,
178
            classes=SourceFileTraceabilityReader.SOURCE_FILE_MODELS,
179
            use_regexp_group=True,
180
        )
181
 
182
    def read(
183
        self, input_string: str, file_path: Optional[str] = None
184
    ) -> SourceFileTraceabilityInfo:
185
        # TODO: This might be possible to handle directly in the textx grammar.
186
        # AttributeError: 'str' object has no attribute '_tx_parser'.
187
        file_size = len(input_string)
188
        if file_size == 0:
189
            return SourceFileTraceabilityInfo([])
190
 
191
        file_stats = SourceFileStats.create(input_string)
192
        parse_context = ParseContext(file_path, file_stats)
193
 
194
        parse_source_traceability_processor = partial(
195
            source_file_traceability_info_processor, parse_context=parse_context
196
        )
197
        parse_req_processor = partial(req_processor)
198
        parse_range_marker_processor = partial(
199
            range_marker_processor, parse_context=parse_context
200
        )
201
        parse_line_marker_processor = partial(
202
            line_marker_processor, parse_context=parse_context
203
        )
204
        parse_language_item_marker_processor = partial(
205
            language_item_marker_processor, parse_context=parse_context
206
        )
207
 
208
        obj_processors = {
209
            "LanguageItemMarker": parse_language_item_marker_processor,
210
            "LineMarker": parse_line_marker_processor,
211
            "RangeMarker": parse_range_marker_processor,
212
            "Req": parse_req_processor,
213
            "SourceFileTraceabilityInfo": parse_source_traceability_processor,
214
        }
215
 
216
        self.meta_model.register_obj_processors(obj_processors)
217
 
218
        source_file_traceability_info: SourceFileTraceabilityInfo = (
219
            self.meta_model.model_from_str(input_string, file_name=file_path)
220
        )
221
        source_file_traceability_info.ng_map_reqs_to_markers = (
222
            parse_context.map_reqs_to_markers
223
        )
224
 
225
        # HACK:
226
        # ProcessPoolExecutor doesn't work because of non-picklable parts
227
        # of textx. The offending fields are stripped down because they
228
        # are not used anyway.
229
        drop_textx_meta(source_file_traceability_info)
230
        return source_file_traceability_info
231
 
232
    def read_from_file(self, file_path: str) -> SourceFileTraceabilityInfo:
233
        with file_open_read_utf8(file_path) as file:
234
            sdoc_content = file.read()
235
            sdoc = self.read(sdoc_content, file_path=file_path)
236
            return sdoc