Skip to content

Commit

Permalink
Merge pull request #1956 from strictdoc-project/stanislaw/functions
Browse files Browse the repository at this point in the history
Feature: Parsing source code functions into requirements graph
  • Loading branch information
stanislaw authored Oct 27, 2024
2 parents 88e4646 + 3fd83cd commit a4db2cc
Show file tree
Hide file tree
Showing 54 changed files with 2,079 additions and 82 deletions.
2 changes: 1 addition & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ omit =
*/.venv/*

[report]
fail_under = 65.0
fail_under = 60.0
precision = 2
skip_covered = true
show_missing = true
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
3.8
3.9

5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ dependencies = [
# Parsing and rendering RST.
"docutils >= 0.16, == 0.*",

# Tree Sitter is used for language/AST-aware parsing of Python, C and other files.
"tree-sitter",
"tree-sitter-c",
"tree-sitter-python",

# Requirements-to-source traceability. Colored syntax for source files.
"pygments >= 2.10.0, == 2.*",

Expand Down
10 changes: 9 additions & 1 deletion strictdoc/backend/sdoc/error_handling.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# mypy: disable-error-code="attr-defined,no-untyped-call,no-untyped-def,union-attr"
from typing import Optional

from textx import TextXSyntaxError

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

class StrictDocSemanticError(Exception):
def __init__(
self, title, hint, example, line=None, col=None, filename=None
self,
title: str,
hint: Optional[str],
example: Optional[str],
line: Optional[int] = None,
col: Optional[int] = None,
filename: Optional[str] = None,
):
super().__init__(title, hint, line, col, filename)
self.title = title
Expand Down
1 change: 1 addition & 0 deletions strictdoc/backend/sdoc/grammar/type_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
(' FORMAT: ' g_file_format = FileEntryFormat '\n')?
' VALUE: ' g_file_path = /.*$/ '\n'
(' LINE_RANGE: ' g_line_range = /.*$/ '\n')?
(' FUNCTION: ' function = /.*$/ '\n')?
;
FileEntryFormat[noskipws]:
Expand Down
6 changes: 6 additions & 0 deletions strictdoc/backend/sdoc/models/type_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def __init__(
g_file_format: Optional[str],
g_file_path: str,
g_line_range: Optional[str],
function: Optional[str],
):
self.parent = parent

Expand Down Expand Up @@ -85,6 +86,11 @@ def __init__(
int(range_components_str[1]),
)

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


class FileEntryFormat:
SOURCECODE = "Sourcecode"
Expand Down
104 changes: 104 additions & 0 deletions strictdoc/backend/sdoc_source_code/marker_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import re
from typing import List, Union

from strictdoc.backend.sdoc_source_code.models.function_range_marker import (
FunctionRangeMarker,
)
from strictdoc.backend.sdoc_source_code.models.range_marker import (
LineMarker,
RangeMarker,
)
from strictdoc.backend.sdoc_source_code.models.requirement_marker import Req

REGEX_REQ = r"[A-Za-z][A-Za-z0-9\\-]+"
# @relation(REQ-1, scope=function)
REGEX_FUNCTION = (
rf"@relation\((/?)({REGEX_REQ}(?:, {REGEX_REQ})*)\, scope=function\)"
)
REGEX_RANGE = rf"@sdoc\[(/?)({REGEX_REQ}(?:, {REGEX_REQ})*)\]"
REGEX_LINE = rf"@sdoc\((/?)({REGEX_REQ}(?:, {REGEX_REQ})*)\)"


class MarkerParser:
@staticmethod
def parse(
input_string: str,
line_start: int,
line_end: int,
comment_line_start: int,
comment_column_start: int,
) -> List[Union[FunctionRangeMarker, RangeMarker, LineMarker]]:
markers: List[Union[FunctionRangeMarker, RangeMarker, LineMarker]] = []
for input_line_idx_, input_line_ in enumerate(
input_string.splitlines()
):
match_function = None
match_line = None
match_range = None

match_function = re.search(REGEX_FUNCTION, input_line_)
if match_function is None:
match_range = re.search(REGEX_RANGE, input_line_)
if match_range is None:
match_line = re.search(REGEX_LINE, input_line_)

match = (
match_function
if match_function is not None
else match_range
if match_range is not None
else match_line
)
if match is None:
continue

start_or_end = match.group(1) != "/"
req_list = match.group(2)

first_requirement_index = match.start(2)

current_line = comment_line_start + input_line_idx_
first_requirement_column = first_requirement_index + 1
if input_line_idx_ == 0:
first_requirement_column += comment_column_start - 1
requirements = []
for req_match in re.finditer(REGEX_REQ, req_list):
req_item = req_match.group(0) # Matched REQ-XXX item
# Calculate actual position relative to the original string
start_index = (
req_match.start()
) # Offset by where group 1 starts
requirement = Req(None, req_item)
requirement.ng_source_line = current_line
requirement.ng_source_column = (
first_requirement_column + start_index
)
requirements.append(requirement)

if match_function is not None:
function_marker = FunctionRangeMarker(None, requirements)
function_marker.ng_source_line_begin = line_start
function_marker.ng_range_line_begin = line_start
function_marker.ng_range_line_end = line_end
function_marker.ng_marker_line = current_line
function_marker.ng_marker_column = first_requirement_column
markers.append(function_marker)
elif match_range is not None:
range_marker = RangeMarker(
None, "[" if start_or_end else "[/", requirements
)
range_marker.ng_source_line_begin = line_start
range_marker.ng_source_column_begin = first_requirement_column
range_marker.ng_range_line_begin = line_start
range_marker.ng_range_line_end = line_end
markers.append(range_marker)
elif match_line is not None:
line_marker = LineMarker(None, requirements)
line_marker.ng_source_line_begin = line_start
line_marker.ng_range_line_begin = line_start
line_marker.ng_range_line_end = line_end
markers.append(line_marker)
else:
continue

return markers
20 changes: 20 additions & 0 deletions strictdoc/backend/sdoc_source_code/models/function.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from typing import Any, List

from strictdoc.helpers.auto_described import auto_described


@auto_described
class Function:
def __init__(
self,
parent: Any,
name: str,
line_begin: int,
line_end: int,
parts: List[Any],
):
self.parent = parent
self.name = name
self.parts: List[Any] = parts
self.line_begin = line_begin
self.line_end = line_end
52 changes: 52 additions & 0 deletions strictdoc/backend/sdoc_source_code/models/function_range_marker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# mypy: disable-error-code="no-untyped-def,type-arg"
from typing import List, Optional

from strictdoc.backend.sdoc_source_code.models.requirement_marker import Req
from strictdoc.helpers.auto_described import auto_described


@auto_described
class FunctionRangeMarker:
def __init__(self, parent, reqs_objs: List[Req]):
assert isinstance(reqs_objs, list)
self.parent = parent
self.reqs_objs: List[Req] = reqs_objs
self.reqs: List[str] = list(map(lambda req: req.uid, reqs_objs))

# Line number of the marker in the source code.
self.ng_source_line_begin: Optional[int] = None
self.ng_source_column_begin: Optional[int] = None

# Line number of the marker range in the source code:
# TODO: Improve description.
# For Begin ranges:
# ng_range_line_begin == ng_source_line_begin # noqa: ERA001
# ng_range_line_end == ng_source_line_begin of the End marker # noqa: ERA001, E501
# For End ranges:
# ng_range_line_begin == ng_range_line_begin of the Begin marker # noqa: ERA001, E501
# ng_range_line_end == ng_source_line_begin # noqa: ERA001
self.ng_range_line_begin: Optional[int] = None
self.ng_range_line_end: Optional[int] = None

self.ng_marker_line: Optional[int] = None
self.ng_marker_column: Optional[int] = None

self.ng_is_nodoc = "nosdoc" in self.reqs

def is_range_marker(self) -> bool:
return True

def is_line_marker(self) -> bool:
return False

def is_begin(self) -> bool:
return True

def is_end(self) -> bool:
return False


@auto_described
class ForwardFunctionRangeMarker(FunctionRangeMarker):
def __init__(self, parent, reqs_objs: List[Req]):
super().__init__(parent, reqs_objs)
54 changes: 28 additions & 26 deletions strictdoc/backend/sdoc_source_code/models/range_marker.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
# mypy: disable-error-code="no-untyped-def,type-arg"
from typing import List
from typing import Any, List, Optional

from strictdoc.backend.sdoc_source_code.models.requirement_marker import Req
from strictdoc.helpers.auto_described import auto_described


@auto_described
class RangeMarker:
def __init__(self, parent, begin_or_end, reqs_objs: List[Req]):
def __init__(self, parent: Any, begin_or_end: str, reqs_objs: List[Req]):
assert isinstance(reqs_objs, list)
self.parent = parent
self.begin_or_end = begin_or_end
self.parent: Any = parent
self.begin_or_end: str = begin_or_end
self.reqs_objs: List[Req] = reqs_objs
self.reqs: List[str] = list(map(lambda req: req.uid, reqs_objs))

# Line number of the marker in the source code.
self.ng_source_line_begin = None
self.ng_source_line_begin: Optional[int] = None
self.ng_source_column_begin: Optional[int] = None

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

self.ng_is_nodoc = "nosdoc" in self.reqs

def is_begin(self):
def is_begin(self) -> bool:
return self.begin_or_end == "["

def is_end(self):
def is_end(self) -> bool:
return self.begin_or_end == "[/"

def is_range_marker(self):
def is_range_marker(self) -> bool:
return True

def is_line_marker(self):
def is_line_marker(self) -> bool:
return False


@auto_described
class LineMarker:
def __init__(self, parent, reqs_objs):
def __init__(self, parent: Any, reqs_objs: List[Req]) -> None:
assert isinstance(reqs_objs, list)
self.parent = parent
self.reqs_objs = reqs_objs
self.reqs = list(map(lambda req: req.uid, reqs_objs))

# Line number of the marker in the source code.
self.ng_source_line_begin = None
self.ng_source_line_begin: Optional[int] = None
self.ng_source_column_begin: Optional[int] = None

self.ng_range_line_begin = None
self.ng_range_line_end = None
self.ng_range_line_begin: Optional[int] = None
self.ng_range_line_end: Optional[int] = None

self.ng_is_nodoc = "nosdoc" in self.reqs

def is_begin(self):
def is_begin(self) -> bool:
return True

def is_end(self):
def is_end(self) -> bool:
return False

def is_range_marker(self):
def is_range_marker(self) -> bool:
return False

def is_line_marker(self):
def is_line_marker(self) -> bool:
return True


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

# Line number of the marker in the source code.
self.ng_source_line_begin = None
self.ng_source_line_begin: Optional[int] = None

self.ng_range_line_begin = None
self.ng_range_line_end = None
self.ng_range_line_begin: Optional[int] = None
self.ng_range_line_end: Optional[int] = None

def is_begin(self):
def is_begin(self) -> bool:
return self.start_or_end

def is_end(self):
def is_end(self) -> bool:
return not self.start_or_end

def is_range_marker(self):
def is_range_marker(self) -> bool:
return True

def is_line_marker(self):
def is_line_marker(self) -> bool:
return False
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
# mypy: disable-error-code="no-untyped-def"
from typing import Any, Optional

from strictdoc.helpers.auto_described import auto_described


@auto_described
class Req:
def __init__(self, parent, uid: str):
def __init__(self, parent: Any, uid: str):
assert isinstance(uid, str)
assert len(uid) > 0

self.parent = parent
self.uid: str = uid

self.ng_source_line = None
self.ng_source_column = None
self.ng_source_line: Optional[int] = None
self.ng_source_column: Optional[int] = None
Loading

0 comments on commit a4db2cc

Please sign in to comment.