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%)
- "7.2.1. Language-aware parsing of source code" (REQUIREMENT)
- "7.6.1. Relation markers syntax" (REQUIREMENT)
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
return82
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
return89
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
return142
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
return149
150
parse_context.markers.append(language_item_marker)
151
152
# Language item markers supported by this general reader can only153
# be of scope=file. Only the language-aware parsing results in154
# 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
@staticmethod172
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 parts227
# of textx. The offending fields are stripped down because they228
# 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