Skip to content

Commit

Permalink
backend/sdoc_source_code: recognize C++ class function declarations/d…
Browse files Browse the repository at this point in the history
…efinitions
  • Loading branch information
stanislaw committed Nov 23, 2024
1 parent b4ea312 commit 040ad48
Show file tree
Hide file tree
Showing 14 changed files with 447 additions and 21 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ dependencies = [

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

# Requirements-to-source traceability. Colored syntax for source files.
Expand Down
9 changes: 8 additions & 1 deletion strictdoc/backend/sdoc_source_code/caching_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ def _get_reader(
if project_config.is_activated_source_file_language_parsers():
if path_to_file.endswith(".py"):
return SourceFileTraceabilityReader_Python()
if path_to_file.endswith(".c") or path_to_file.endswith(".h"):
if (
path_to_file.endswith(".c")
or path_to_file.endswith(".cc")
or path_to_file.endswith(".h")
or path_to_file.endswith(".hh")
or path_to_file.endswith(".hpp")
or path_to_file.endswith(".cpp")
):
return SourceFileTraceabilityReader_C()
return SourceFileTraceabilityReader()
110 changes: 94 additions & 16 deletions strictdoc/backend/sdoc_source_code/reader_c.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# mypy: disable-error-code="no-redef,no-untyped-call,no-untyped-def,type-arg,var-annotated"
import sys
import traceback
from typing import List, Optional, Union
from typing import List, Optional, Sequence, Union

import tree_sitter_c
import tree_sitter_cpp
from tree_sitter import Language, Node, Parser

from strictdoc.backend.sdoc.error_handling import StrictDocSemanticError
Expand All @@ -12,6 +12,7 @@
from strictdoc.backend.sdoc_source_code.models.function import Function
from strictdoc.backend.sdoc_source_code.models.function_range_marker import (
FunctionRangeMarker,
RangeMarkerType,
)
from strictdoc.backend.sdoc_source_code.models.range_marker import (
LineMarker,
Expand Down Expand Up @@ -52,7 +53,7 @@ def read(
parse_context = ParseContext(file_path, length)

# Works since Python 3.9 but we also lint this with mypy from Python 3.8.
language_arg = tree_sitter_c.language()
language_arg = tree_sitter_cpp.language()
py_language = Language( # type: ignore[call-arg, unused-ignore]
language_arg
)
Expand Down Expand Up @@ -82,6 +83,12 @@ def read(
node_.start_point[1] + 1,
)
for marker_ in markers:
if not isinstance(marker_, FunctionRangeMarker):
continue
# At the top level, only accept the scope=file markers.
# Everything else will be handled by functions and classes.
if marker_.scope != RangeMarkerType.FILE:
continue
if isinstance(marker_, FunctionRangeMarker) and (
function_range_marker_ := marker_
):
Expand All @@ -91,15 +98,40 @@ def read(
traceability_info.markers.append(
function_range_marker_
)
elif node_.type == "declaration":

elif node_.type in ("declaration", "field_declaration"):
function_declarator_node = ts_find_child_node_by_type(
node_, "function_declarator"
)

# C++ reference declaration wrap the function declaration one time.
if function_declarator_node is None:
continue
# Example: TrkVertex& operator-=(const TrkVertex& c);
reference_declarator_node = ts_find_child_node_by_type(
node_, "reference_declarator"
)
if reference_declarator_node is None:
continue

function_declarator_node = ts_find_child_node_by_type(
reference_declarator_node, "function_declarator"
)
if function_declarator_node is None:
continue

# For normal C functions the identifier is "identifier".
# For C++, there are:
# Class function declarations: bool CanSend(const CanFrame &frame); # noqa: ERA001
# Operators: TrkVertex& operator-=(const TrkVertex& c); # noqa: ERA001
# Destructors: ~TrkVertex(); # noqa: ERA001
function_identifier_node = ts_find_child_node_by_type(
function_declarator_node, "identifier"
function_declarator_node,
node_type=(
"identifier",
"field_identifier",
"operator_name",
"destructor_name",
),
)
if function_identifier_node is None:
continue
Expand All @@ -110,7 +142,15 @@ def read(
function_name: str = function_identifier_node.text.decode(
"utf8"
)
assert function_name is not None, function_name
assert (
function_name is not None
), "function_name must not be None"

parent_names = self.get_node_ns(node_)
if len(parent_names) > 0:
function_name = (
f"{'::'.join(parent_names)}::{function_name}"
)

function_attributes = {FunctionAttribute.DECLARATION}
for specifier_node_ in ts_find_child_nodes_by_type(
Expand Down Expand Up @@ -173,11 +213,28 @@ def read(

for child_ in node_.children:
if child_.type == "function_declarator":
assert child_.children[0].type == "identifier"
assert child_.children[0].text

function_name = child_.children[0].text.decode("utf8")
assert function_name is not None, "Function name"
identifier_node = ts_find_child_node_by_type(
child_,
(
"identifier",
"qualified_identifier",
"destructor_name",
),
)
if identifier_node is None:
raise NotImplementedError(child_)

assert identifier_node.text is not None
function_name = identifier_node.text.decode("utf8")

assert (
function_name is not None
), "Could not parse function name"
parent_names = self.get_node_ns(node_)
if len(parent_names) > 0:
function_name = (
f"{'::'.join(parent_names)}::{function_name}"
)

function_markers: List[FunctionRangeMarker] = []
function_comment_node: Optional[Node] = None
Expand All @@ -187,7 +244,9 @@ def read(
and node_.prev_sibling.type == "comment"
):
function_comment_node = node_.prev_sibling
assert function_comment_node.text is not None
assert (
function_comment_node.text is not None
), function_comment_node
function_comment_text = function_comment_node.text.decode(
"utf8"
)
Expand Down Expand Up @@ -287,9 +346,28 @@ def read_from_file(self, file_path):
sys.exit(1)
except Exception as exc: # pylint: disable=broad-except
print( # noqa: T201
f"error: SourceFileTraceabilityReader_Python: could not parse file: "
f"error: SourceFileTraceabilityReader_C: could not parse file: "
f"{file_path}.\n{exc.__class__.__name__}: {exc}"
)
# TODO: when --debug is provided
# traceback.print_exc() # noqa: ERA001
traceback.print_exc()
sys.exit(1)

@staticmethod
def get_node_ns(node: Node) -> Sequence[str]:
"""Walk up the tree and find parent classes"""
parent_scopes = []
cursor: Optional[Node] = node
while cursor is not None:
if cursor.type == "class_specifier" and len(cursor.children) > 1:
second_node_or_none = cursor.children[1]
if (
second_node_or_none.type == "type_identifier"
and second_node_or_none.text is not None
):
parent_class_name = second_node_or_none.text.decode("utf8")
parent_scopes.append(parent_class_name)

cursor = cursor.parent

parent_scopes.reverse()
return parent_scopes
11 changes: 8 additions & 3 deletions strictdoc/backend/sdoc_source_code/tree_sitter_helpers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Generator, Optional
from typing import Generator, Optional, Tuple, Union

from tree_sitter import Node, Tree

Expand All @@ -18,9 +18,14 @@ def traverse_tree(tree: Tree) -> Generator[Node, None, None]:
break


def ts_find_child_node_by_type(node: Node, node_type: str) -> Optional[Node]:
def ts_find_child_node_by_type(
node: Node, node_type: Union[str, Tuple[str, ...], str]
) -> Optional[Node]:
node_types: Tuple[str, ...] = (
node_type if isinstance(node_type, tuple) else (node_type,)
)
for child_ in node.children:
if child_.type == node_type:
if child_.type in node_types:
return child_
return None

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class EcuHal
{
public:
// @relation(REQ-1, scope=function)
bool CanSend(const CanFrame &frame);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[DOCUMENT]
TITLE: Hello world doc

[REQUIREMENT]
UID: REQ-1
TITLE: Requirement Title
STATEMENT: Requirement Statement
RELATIONS:
- TYPE: File
VALUE: file.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]

features = [
"REQUIREMENT_TO_SOURCE_TRACEABILITY",
"SOURCE_FILE_LANGUAGE_PARSERS",
"PROJECT_STATISTICS_SCREEN"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
REQUIRES: PYTHON_39_OR_HIGHER

RUN: %strictdoc export %S --output-dir Output | filecheck %s --dump-input=fail
CHECK: Published: Hello world doc

RUN: %check_exists --file "%S/Output/html/_source_files/file.hpp.html"

RUN: %cat %S/Output/html/%THIS_TEST_FOLDER/input.html | filecheck %s --dump-input=fail --check-prefix CHECK-HTML
CHECK-HTML: <a{{.*}}href="../_source_files/file.hpp.html#REQ-1#4#5">

RUN: %cat %S/Output/html/_source_files/file.hpp.html | filecheck %s --dump-input=fail --check-prefix CHECK-SOURCE-FILE
CHECK-SOURCE-FILE: <a{{.*}}href="../{{.*}}/input.html#1-REQ-1"{{.*}}>

RUN: %cat %S/Output/html/source_coverage.html | filecheck %s --dump-input=fail --check-prefix CHECK-SOURCE-COVERAGE
CHECK-SOURCE-COVERAGE: 33.3%
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
bool EcuHal::CanSend(const CanFrame &frame) {
return true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class EcuHal
{
public:
// @relation(REQ-1, scope=function)
bool CanSend(const CanFrame &frame);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[DOCUMENT]
TITLE: Hello world doc

[REQUIREMENT]
UID: REQ-1
TITLE: Requirement Title
STATEMENT: Requirement Statement
RELATIONS:
- TYPE: File
VALUE: file.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]

features = [
"REQUIREMENT_TO_SOURCE_TRACEABILITY",
"SOURCE_FILE_LANGUAGE_PARSERS",
"PROJECT_STATISTICS_SCREEN"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
REQUIRES: PYTHON_39_OR_HIGHER

RUN: %strictdoc export %S --output-dir Output | filecheck %s --dump-input=fail
CHECK: Published: Hello world doc

RUN: %check_exists --file "%S/Output/html/_source_files/file.hpp.html"

RUN: %cat %S/Output/html/%THIS_TEST_FOLDER/input.html | filecheck %s --dump-input=fail --check-prefix CHECK-HTML
CHECK-HTML: <a{{.*}}href="../_source_files/file.cpp.html#REQ-1#1#3">
CHECK-HTML: <a{{.*}}href="../_source_files/file.hpp.html#REQ-1#4#5">

RUN: %cat %S/Output/html/_source_files/file.hpp.html | filecheck %s --dump-input=fail --check-prefix CHECK-SOURCE-FILE
CHECK-SOURCE-FILE: <a{{.*}}href="../{{.*}}/input.html#1-REQ-1"{{.*}}>

RUN: %cat %S/Output/html/source_coverage.html | filecheck %s --dump-input=fail --check-prefix CHECK-SOURCE-COVERAGE
CHECK-SOURCE-COVERAGE: 33.3%
Loading

0 comments on commit 040ad48

Please sign in to comment.