Skip to content

Commit 6006290

Browse files
committed
Feature: Parsing source code functions into requirements graph
1 parent 88e4646 commit 6006290

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+2079
-82
lines changed

.coveragerc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ omit =
55
*/.venv/*
66

77
[report]
8-
fail_under = 65.0
8+
fail_under = 60.0
99
precision = 2
1010
skip_covered = true
1111
show_missing = true

.python-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
3.8
1+
3.9
22

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ dependencies = [
5656
# Parsing and rendering RST.
5757
"docutils >= 0.16, == 0.*",
5858

59+
# Tree Sitter is used for language/AST-aware parsing of Python, C and other files.
60+
"tree-sitter",
61+
"tree-sitter-c",
62+
"tree-sitter-python",
63+
5964
# Requirements-to-source traceability. Colored syntax for source files.
6065
"pygments >= 2.10.0, == 2.*",
6166

strictdoc/backend/sdoc/error_handling.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# mypy: disable-error-code="attr-defined,no-untyped-call,no-untyped-def,union-attr"
2+
from typing import Optional
3+
24
from textx import TextXSyntaxError
35

46
from strictdoc.backend.sdoc.models.document import SDocDocument
@@ -25,7 +27,13 @@ def get_textx_syntax_error_message(exception: TextXSyntaxError):
2527

2628
class StrictDocSemanticError(Exception):
2729
def __init__(
28-
self, title, hint, example, line=None, col=None, filename=None
30+
self,
31+
title: str,
32+
hint: Optional[str],
33+
example: Optional[str],
34+
line: Optional[int] = None,
35+
col: Optional[int] = None,
36+
filename: Optional[str] = None,
2937
):
3038
super().__init__(title, hint, line, col, filename)
3139
self.title = title

strictdoc/backend/sdoc/grammar/type_system.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
(' FORMAT: ' g_file_format = FileEntryFormat '\n')?
4848
' VALUE: ' g_file_path = /.*$/ '\n'
4949
(' LINE_RANGE: ' g_line_range = /.*$/ '\n')?
50+
(' FUNCTION: ' function = /.*$/ '\n')?
5051
;
5152
5253
FileEntryFormat[noskipws]:

strictdoc/backend/sdoc/models/type_system.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def __init__(
5656
g_file_format: Optional[str],
5757
g_file_path: str,
5858
g_line_range: Optional[str],
59+
function: Optional[str],
5960
):
6061
self.parent = parent
6162

@@ -85,6 +86,11 @@ def __init__(
8586
int(range_components_str[1]),
8687
)
8788

89+
# textX parses an optional element as an empty string. We make it to None ourselves.
90+
self.function: Optional[str] = (
91+
function if function is not None and len(function) > 0 else None
92+
)
93+
8894

8995
class FileEntryFormat:
9096
SOURCECODE = "Sourcecode"
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import re
2+
from typing import List, Union
3+
4+
from strictdoc.backend.sdoc_source_code.models.function_range_marker import (
5+
FunctionRangeMarker,
6+
)
7+
from strictdoc.backend.sdoc_source_code.models.range_marker import (
8+
LineMarker,
9+
RangeMarker,
10+
)
11+
from strictdoc.backend.sdoc_source_code.models.requirement_marker import Req
12+
13+
REGEX_REQ = r"[A-Za-z][A-Za-z0-9\\-]+"
14+
# @relation(REQ-1, scope=function)
15+
REGEX_FUNCTION = (
16+
rf"@relation\((/?)({REGEX_REQ}(?:, {REGEX_REQ})*)\, scope=function\)"
17+
)
18+
REGEX_RANGE = rf"@sdoc\[(/?)({REGEX_REQ}(?:, {REGEX_REQ})*)\]"
19+
REGEX_LINE = rf"@sdoc\((/?)({REGEX_REQ}(?:, {REGEX_REQ})*)\)"
20+
21+
22+
class MarkerParser:
23+
@staticmethod
24+
def parse(
25+
input_string: str,
26+
line_start: int,
27+
line_end: int,
28+
comment_line_start: int,
29+
comment_column_start: int,
30+
) -> List[Union[FunctionRangeMarker, RangeMarker, LineMarker]]:
31+
markers: List[Union[FunctionRangeMarker, RangeMarker, LineMarker]] = []
32+
for input_line_idx_, input_line_ in enumerate(
33+
input_string.splitlines()
34+
):
35+
match_function = None
36+
match_line = None
37+
match_range = None
38+
39+
match_function = re.search(REGEX_FUNCTION, input_line_)
40+
if match_function is None:
41+
match_range = re.search(REGEX_RANGE, input_line_)
42+
if match_range is None:
43+
match_line = re.search(REGEX_LINE, input_line_)
44+
45+
match = (
46+
match_function
47+
if match_function is not None
48+
else match_range
49+
if match_range is not None
50+
else match_line
51+
)
52+
if match is None:
53+
continue
54+
55+
start_or_end = match.group(1) != "/"
56+
req_list = match.group(2)
57+
58+
first_requirement_index = match.start(2)
59+
60+
current_line = comment_line_start + input_line_idx_
61+
first_requirement_column = first_requirement_index + 1
62+
if input_line_idx_ == 0:
63+
first_requirement_column += comment_column_start - 1
64+
requirements = []
65+
for req_match in re.finditer(REGEX_REQ, req_list):
66+
req_item = req_match.group(0) # Matched REQ-XXX item
67+
# Calculate actual position relative to the original string
68+
start_index = (
69+
req_match.start()
70+
) # Offset by where group 1 starts
71+
requirement = Req(None, req_item)
72+
requirement.ng_source_line = current_line
73+
requirement.ng_source_column = (
74+
first_requirement_column + start_index
75+
)
76+
requirements.append(requirement)
77+
78+
if match_function is not None:
79+
function_marker = FunctionRangeMarker(None, requirements)
80+
function_marker.ng_source_line_begin = line_start
81+
function_marker.ng_range_line_begin = line_start
82+
function_marker.ng_range_line_end = line_end
83+
function_marker.ng_marker_line = current_line
84+
function_marker.ng_marker_column = first_requirement_column
85+
markers.append(function_marker)
86+
elif match_range is not None:
87+
range_marker = RangeMarker(
88+
None, "[" if start_or_end else "[/", requirements
89+
)
90+
range_marker.ng_source_line_begin = line_start
91+
range_marker.ng_source_column_begin = first_requirement_column
92+
range_marker.ng_range_line_begin = line_start
93+
range_marker.ng_range_line_end = line_end
94+
markers.append(range_marker)
95+
elif match_line is not None:
96+
line_marker = LineMarker(None, requirements)
97+
line_marker.ng_source_line_begin = line_start
98+
line_marker.ng_range_line_begin = line_start
99+
line_marker.ng_range_line_end = line_end
100+
markers.append(line_marker)
101+
else:
102+
continue
103+
104+
return markers
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from typing import Any, List
2+
3+
from strictdoc.helpers.auto_described import auto_described
4+
5+
6+
@auto_described
7+
class Function:
8+
def __init__(
9+
self,
10+
parent: Any,
11+
name: str,
12+
line_begin: int,
13+
line_end: int,
14+
parts: List[Any],
15+
):
16+
self.parent = parent
17+
self.name = name
18+
self.parts: List[Any] = parts
19+
self.line_begin = line_begin
20+
self.line_end = line_end
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# mypy: disable-error-code="no-untyped-def,type-arg"
2+
from typing import List, Optional
3+
4+
from strictdoc.backend.sdoc_source_code.models.requirement_marker import Req
5+
from strictdoc.helpers.auto_described import auto_described
6+
7+
8+
@auto_described
9+
class FunctionRangeMarker:
10+
def __init__(self, parent, reqs_objs: List[Req]):
11+
assert isinstance(reqs_objs, list)
12+
self.parent = parent
13+
self.reqs_objs: List[Req] = reqs_objs
14+
self.reqs: List[str] = list(map(lambda req: req.uid, reqs_objs))
15+
16+
# Line number of the marker in the source code.
17+
self.ng_source_line_begin: Optional[int] = None
18+
self.ng_source_column_begin: Optional[int] = None
19+
20+
# Line number of the marker range in the source code:
21+
# TODO: Improve description.
22+
# For Begin ranges:
23+
# ng_range_line_begin == ng_source_line_begin # noqa: ERA001
24+
# ng_range_line_end == ng_source_line_begin of the End marker # noqa: ERA001, E501
25+
# For End ranges:
26+
# ng_range_line_begin == ng_range_line_begin of the Begin marker # noqa: ERA001, E501
27+
# ng_range_line_end == ng_source_line_begin # noqa: ERA001
28+
self.ng_range_line_begin: Optional[int] = None
29+
self.ng_range_line_end: Optional[int] = None
30+
31+
self.ng_marker_line: Optional[int] = None
32+
self.ng_marker_column: Optional[int] = None
33+
34+
self.ng_is_nodoc = "nosdoc" in self.reqs
35+
36+
def is_range_marker(self) -> bool:
37+
return True
38+
39+
def is_line_marker(self) -> bool:
40+
return False
41+
42+
def is_begin(self) -> bool:
43+
return True
44+
45+
def is_end(self) -> bool:
46+
return False
47+
48+
49+
@auto_described
50+
class ForwardFunctionRangeMarker(FunctionRangeMarker):
51+
def __init__(self, parent, reqs_objs: List[Req]):
52+
super().__init__(parent, reqs_objs)
Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
# mypy: disable-error-code="no-untyped-def,type-arg"
2-
from typing import List
2+
from typing import Any, List, Optional
33

44
from strictdoc.backend.sdoc_source_code.models.requirement_marker import Req
55
from strictdoc.helpers.auto_described import auto_described
66

77

88
@auto_described
99
class RangeMarker:
10-
def __init__(self, parent, begin_or_end, reqs_objs: List[Req]):
10+
def __init__(self, parent: Any, begin_or_end: str, reqs_objs: List[Req]):
1111
assert isinstance(reqs_objs, list)
12-
self.parent = parent
13-
self.begin_or_end = begin_or_end
12+
self.parent: Any = parent
13+
self.begin_or_end: str = begin_or_end
1414
self.reqs_objs: List[Req] = reqs_objs
1515
self.reqs: List[str] = list(map(lambda req: req.uid, reqs_objs))
1616

1717
# Line number of the marker in the source code.
18-
self.ng_source_line_begin = None
18+
self.ng_source_line_begin: Optional[int] = None
19+
self.ng_source_column_begin: Optional[int] = None
1920

2021
# Line number of the marker range in the source code:
2122
# TODO: Improve description.
@@ -25,50 +26,51 @@ def __init__(self, parent, begin_or_end, reqs_objs: List[Req]):
2526
# For End ranges:
2627
# ng_range_line_begin == ng_range_line_begin of the Begin marker # noqa: ERA001, E501
2728
# ng_range_line_end == ng_source_line_begin # noqa: ERA001
28-
self.ng_range_line_begin = None
29-
self.ng_range_line_end = None
29+
self.ng_range_line_begin: Optional[int] = None
30+
self.ng_range_line_end: Optional[int] = None
3031

3132
self.ng_is_nodoc = "nosdoc" in self.reqs
3233

33-
def is_begin(self):
34+
def is_begin(self) -> bool:
3435
return self.begin_or_end == "["
3536

36-
def is_end(self):
37+
def is_end(self) -> bool:
3738
return self.begin_or_end == "[/"
3839

39-
def is_range_marker(self):
40+
def is_range_marker(self) -> bool:
4041
return True
4142

42-
def is_line_marker(self):
43+
def is_line_marker(self) -> bool:
4344
return False
4445

4546

4647
@auto_described
4748
class LineMarker:
48-
def __init__(self, parent, reqs_objs):
49+
def __init__(self, parent: Any, reqs_objs: List[Req]) -> None:
4950
assert isinstance(reqs_objs, list)
5051
self.parent = parent
5152
self.reqs_objs = reqs_objs
5253
self.reqs = list(map(lambda req: req.uid, reqs_objs))
5354

5455
# Line number of the marker in the source code.
55-
self.ng_source_line_begin = None
56+
self.ng_source_line_begin: Optional[int] = None
57+
self.ng_source_column_begin: Optional[int] = None
5658

57-
self.ng_range_line_begin = None
58-
self.ng_range_line_end = None
59+
self.ng_range_line_begin: Optional[int] = None
60+
self.ng_range_line_end: Optional[int] = None
5961

6062
self.ng_is_nodoc = "nosdoc" in self.reqs
6163

62-
def is_begin(self):
64+
def is_begin(self) -> bool:
6365
return True
6466

65-
def is_end(self):
67+
def is_end(self) -> bool:
6668
return False
6769

68-
def is_range_marker(self):
70+
def is_range_marker(self) -> bool:
6971
return False
7072

71-
def is_line_marker(self):
73+
def is_line_marker(self) -> bool:
7274
return True
7375

7476

@@ -81,19 +83,19 @@ def __init__(self, start_or_end: bool, reqs_objs: List):
8183
self.reqs_objs = reqs_objs
8284

8385
# Line number of the marker in the source code.
84-
self.ng_source_line_begin = None
86+
self.ng_source_line_begin: Optional[int] = None
8587

86-
self.ng_range_line_begin = None
87-
self.ng_range_line_end = None
88+
self.ng_range_line_begin: Optional[int] = None
89+
self.ng_range_line_end: Optional[int] = None
8890

89-
def is_begin(self):
91+
def is_begin(self) -> bool:
9092
return self.start_or_end
9193

92-
def is_end(self):
94+
def is_end(self) -> bool:
9395
return not self.start_or_end
9496

95-
def is_range_marker(self):
97+
def is_range_marker(self) -> bool:
9698
return True
9799

98-
def is_line_marker(self):
100+
def is_line_marker(self) -> bool:
99101
return False

0 commit comments

Comments
 (0)