Path:
strictdoc/backend/sdoc_source_code/reader_robot.py
Lines:
248
Non-empty lines:
225
Non-empty lines covered with requirements:
225 / 225 (100.0%)
Functions:
14
Functions covered by requirements:
14 / 14 (100.0%)
- "7.2.1. Language-aware parsing of source code" (REQUIREMENT)
- "7.2.5. Language-aware parsing of Robot framework code" (REQUIREMENT)
1
"""2
@relation(SDOC-SRS-142, SDOC-SRS-148, scope=file)3
"""4
5
from typing import List, Optional, Union
6
7
from robot.api.parsing import (
8
Comment,
9
Documentation,
10
EmptyLine,
11
ModelVisitor,
12
Tags,
13
TestCase,
14
Token,
15
get_model,
16
)17
from robot.parsing.model.statements import Statement
18
19
from strictdoc.backend.sdoc_source_code.constants import FunctionAttribute
20
from strictdoc.backend.sdoc_source_code.marker_parser import MarkerParser
21
from strictdoc.backend.sdoc_source_code.models.language import LanguageItem
22
from strictdoc.backend.sdoc_source_code.models.language_item_marker import (
23
LanguageItemMarker,
24
RangeMarkerType,
25
)26
from strictdoc.backend.sdoc_source_code.models.line_marker import LineMarker
27
from strictdoc.backend.sdoc_source_code.models.range_marker import (
28
RangeMarker,
29
)30
from strictdoc.backend.sdoc_source_code.models.source_file_info import (
31
SourceFileTraceabilityInfo,
32
)33
from strictdoc.backend.sdoc_source_code.models.source_node import SourceNode
34
from strictdoc.backend.sdoc_source_code.parse_context import ParseContext
35
from strictdoc.backend.sdoc_source_code.processors.general_language_marker_processors import (
36
language_item_marker_processor,
37
line_marker_processor,
38
range_marker_processor,
39
source_file_traceability_info_processor,
40
)41
from strictdoc.helpers.file_stats import SourceFileStats
42
from strictdoc.helpers.file_system import file_open_read_utf8
43
44
45
class SdocRelationVisitor(ModelVisitor): # type: ignore[misc]
46
"""
47
Create functions from test cases in *.robot files and create markers.48
49
Note: ModelVisitor reuses ast.NodeVisitor from Python. We rely on following50
behavior.51
- It traverses depth first. This order is important to get matching marker52
pairs on the ParsersContext.marker_stack.53
- It doesn't recurse into subtrees if a custom visit_* method is defined.54
This is important to avoid duplicated matches.55
"""56
57
def __init__(
58
self,
59
traceability_info: SourceFileTraceabilityInfo,
60
parse_context: ParseContext,
61
custom_tags: Optional[set[str]] = None,
62
):63
super().__init__()
64
self.traceability_info = traceability_info
65
self.parse_context = parse_context
66
self.custom_tags = custom_tags
67
68
def visit_Comment(self, node: Comment) -> None:
69
"""
70
Create non-function Marker from Comment outside TestCases.71
"""72
self._visit_possibly_marked_node(node)
73
74
def visit_Documentation(self, node: Documentation) -> None:
75
"""
76
Create non-function Marker from Documentation outside TestCases.77
"""78
self._visit_possibly_marked_node(node)
79
80
def visit_Tags(self, node: Tags) -> None:
81
"""
82
Create non-function Marker from Tags outside TestCases.83
"""84
self._visit_possibly_marked_node(node)
85
86
def visit_TestCase(self, node: TestCase) -> None:
87
"""
88
Create function and non-function Marker from TestCases.89
"""90
trailing_empty_lines = 0
91
tc_markers: List[
92
Union[LanguageItemMarker, RangeMarker, LineMarker]
93
] = []
94
tc_source_nodes: List[SourceNode] = []
95
96
for stmt in node.body:
97
if isinstance(stmt, EmptyLine):
98
# Trim trailing newlines from test case range.99
trailing_empty_lines += 1
100
else:
101
trailing_empty_lines = 0
102
103
source_node = self._parse_stmt(
104
stmt, node.name, node.lineno, node.end_lineno
105
)106
if source_node is not None:
107
tc_markers.extend(source_node.markers)
108
if isinstance(stmt, Documentation) and source_node.fields:
109
tc_source_nodes.append(source_node)
110
111
language_item_markers = []
112
for marker_ in tc_markers:
113
if isinstance(marker_, LanguageItemMarker):
114
marker_.ng_range_line_begin = node.lineno
115
marker_.ng_range_line_end = (
116
node.end_lineno - trailing_empty_lines
117
)118
language_item_markers.append(marker_)
119
language_item_marker_processor(marker_, self.parse_context)
120
elif isinstance(marker_, RangeMarker):
121
range_marker_processor(marker_, self.parse_context)
122
elif isinstance(marker_, LineMarker):
123
line_marker_processor(marker_, self.parse_context)
124
125
self.traceability_info.markers.extend(language_item_markers)
126
test_case = LanguageItem(
127
parent=self.traceability_info,
128
name=node.name,
129
display_name=node.name,
130
line_begin=node.lineno,
131
line_end=node.end_lineno - trailing_empty_lines,
132
# FIXME: Byte range is currently not used for Robot framework.133
code_byte_range=None,
134
child_functions=[],
135
markers=language_item_markers,
136
attributes={FunctionAttribute.DEFINITION},
137
)138
# Link source nodes (parsed from [Documentation] key: value fields)139
# to the test case LanguageItem and register them in traceability_info.140
for source_node in tc_source_nodes:
141
source_node.function = test_case
142
self.traceability_info.source_nodes.extend(tc_source_nodes)
143
self.traceability_info.functions.append(test_case)
144
145
def _visit_possibly_marked_node(
146
self, node: Union[Comment, Documentation, Tags]
147
) -> None:
148
source_node = self._parse_stmt(node, None, node.lineno, node.end_lineno)
149
if source_node is None:
150
return151
for marker_ in source_node.markers:
152
if (
153
isinstance(marker_, LanguageItemMarker)
154
and marker_.scope is RangeMarkerType.FILE
155
):156
# Outside Test Cases only accept scope=file function markers157
marker_.ng_range_line_begin = 1
158
marker_.ng_range_line_end = (
159
self.parse_context.file_stats.lines_total
160
)161
language_item_marker_processor(marker_, self.parse_context)
162
self.traceability_info.markers.append(marker_)
163
elif isinstance(marker_, RangeMarker):
164
range_marker_processor(marker_, self.parse_context)
165
elif isinstance(marker_, LineMarker):
166
line_marker_processor(marker_, self.parse_context)
167
168
def _parse_stmt(
169
self,
170
stmt: Statement,
171
entity_name: Optional[str],
172
line_start: int,
173
line_end: int,
174
) -> Optional[SourceNode]:
175
if isinstance(stmt, Documentation):
176
# Documentation is expected to contain relation markers and source nodes.177
# FIXME: No writeback support for source nodes, because178
# 1) Robot parser doesn't track byte-offsets (lines/columns count characters, not bytes),179
# 2) Line continuation format not handled by MarkerParser.180
return MarkerParser.parse(
181
input_string=stmt.value,
182
line_start=line_start,
183
line_end=line_end,
184
comment_byte_range=None,
185
filename=self.parse_context.filename,
186
comment_line_start=stmt.lineno,
187
entity_name=entity_name,
188
col_offset=stmt.col_offset,
189
custom_tags=self.custom_tags,
190
)191
elif isinstance(stmt, (Comment, Tags)):
192
# Expect relation markers but no source nodes in comments and tags to keep things simple.193
source_nodes = SourceNode(
194
entity_name=entity_name, comment_byte_range=None
195
)196
for token in filter(self._token_filter, stmt.tokens):
197
sn = MarkerParser.parse(
198
input_string=token.value,
199
line_start=token.lineno,
200
line_end=token.lineno,
201
comment_byte_range=None,
202
filename=self.parse_context.filename,
203
comment_line_start=token.lineno,
204
entity_name=entity_name,
205
col_offset=token.col_offset,
206
)207
source_nodes.markers.extend(sn.markers)
208
return source_nodes
209
return None
210
211
@staticmethod212
def _token_filter(token: Token) -> bool:
213
if token.type in ("SEPARATOR", "EOL"):
214
return False
215
return True
216
217
218
class SourceFileTraceabilityReader_Robot:
219
@staticmethod220
def supported_elements() -> list[str]:
221
return ["testcase"]
222
223
def __init__(self, custom_tags: Optional[set[str]] = None) -> None:
224
self.custom_tags: Optional[set[str]] = custom_tags
225
226
def read(
227
self, input_buffer: str, file_path: Optional[str] = None
228
) -> SourceFileTraceabilityInfo:
229
traceability_info = SourceFileTraceabilityInfo([])
230
if len(input_buffer) == 0:
231
return traceability_info
232
file_stats = SourceFileStats.create(input_buffer)
233
parse_context = ParseContext(file_path, file_stats)
234
robotfw_model = get_model(input_buffer, data_only=False)
235
visitor = SdocRelationVisitor(
236
traceability_info, parse_context, self.custom_tags
237
)238
visitor.visit(robotfw_model)
239
source_file_traceability_info_processor(
240
traceability_info, parse_context
241
)242
return traceability_info
243
244
def read_from_file(self, file_path: str) -> SourceFileTraceabilityInfo:
245
with file_open_read_utf8(file_path) as file:
246
sdoc_content = file.read()
247
sdoc = self.read(sdoc_content, file_path=file_path)
248
return sdoc