Skip to content

Commit 9de36f3

Browse files
authored
Merge pull request #2524 from strictdoc-project/stanislaw/line_marker
feat(backend/sdoc_source_code): add support for multiline scope=line markers in Python code
2 parents aa92629 + 4fb157c commit 9de36f3

File tree

12 files changed

+224
-9
lines changed

12 files changed

+224
-9
lines changed

strictdoc/backend/sdoc_source_code/marker_parser.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,12 @@ def _parse_relation_marker(
175175
range_marker.ng_source_column_begin = (
176176
element_.meta.column + col_offset
177177
)
178-
range_marker.ng_range_line_begin = line_start
179-
range_marker.ng_range_line_end = line_end
178+
range_marker.ng_range_line_begin = (
179+
comment_line_start + element_.meta.line - 1
180+
)
181+
range_marker.ng_range_line_end = (
182+
comment_line_start + element_.meta.end_line - 1
183+
)
180184
markers.append(range_marker)
181185
elif relation_scope == "line":
182186
line_marker = LineMarker(None, requirements, role=relation_role)
@@ -186,8 +190,12 @@ def _parse_relation_marker(
186190
line_marker.ng_source_column_begin = (
187191
element_.meta.column + col_offset
188192
)
189-
line_marker.ng_range_line_begin = line_start
190-
line_marker.ng_range_line_end = line_end + 1
193+
line_marker.ng_range_line_begin = (
194+
comment_line_start + element_.meta.line - 1
195+
)
196+
line_marker.ng_range_line_end = (
197+
comment_line_start + element_.meta.end_line
198+
)
191199
markers.append(line_marker)
192200
else:
193201
raise NotImplementedError

strictdoc/backend/sdoc_source_code/reader_python.py

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44

55
from itertools import islice
6-
from typing import List, Optional, Sequence
6+
from typing import Any, List, Optional, Sequence, Tuple
77

88
import tree_sitter_python
99
from tree_sitter import Language, Node, Parser
@@ -62,6 +62,8 @@ def read(
6262

6363
nodes = traverse_tree(tree)
6464
map_function_to_node = {}
65+
66+
visited_comments = set()
6567
for node_ in nodes:
6668
if node_.type == "module":
6769
function = Function(
@@ -214,16 +216,34 @@ def read(
214216
function_markers
215217
)
216218
elif node_.type == "comment":
219+
if node_ in visited_comments:
220+
continue
221+
222+
assert node_.parent is not None
217223
assert node_.text is not None, (
218224
f"Comment without a text: {node_}"
219225
)
220226

221-
node_text_string = node_.text.decode("utf8")
227+
if not SourceFileTraceabilityReader_Python.is_comment_alone_on_line(
228+
node_
229+
):
230+
continue
231+
232+
merged_comments, last_idx = (
233+
SourceFileTraceabilityReader_Python.collect_consecutive_comments(
234+
node_
235+
)
236+
)
237+
238+
for j in range(node_.parent.children.index(node_), last_idx):
239+
visited_comments.add(node_.parent.children[j])
240+
241+
last_comment = node_.parent.children[last_idx - 1]
222242

223243
source_node = MarkerParser.parse(
224-
node_text_string,
244+
merged_comments,
225245
node_.start_point[0] + 1,
226-
node_.end_point[0] + 1,
246+
last_comment.end_point[0] + 1,
227247
node_.start_point[0] + 1,
228248
None,
229249
)
@@ -286,3 +306,62 @@ def get_node_ns(node: Node) -> Sequence[str]:
286306
# The array now contains the "fully qualified" node name,
287307
# we want to return the namespace, so don't return the last part.
288308
return parent_scopes[:-1]
309+
310+
@staticmethod
311+
def collect_consecutive_comments(comment_node: Any) -> Tuple[str, int]:
312+
parent = comment_node.parent
313+
314+
siblings = parent.children
315+
idx = siblings.index(comment_node)
316+
317+
merged_texts = []
318+
319+
last_node = None
320+
321+
while idx < len(siblings) and siblings[idx].type == "comment":
322+
n = siblings[idx]
323+
assert n.text is not None
324+
text = n.text.decode("utf8")
325+
326+
if last_node is not None:
327+
# Tree-sitter line numbers are 0-based
328+
last_end_line = last_node.end_point[0]
329+
curr_start_line = n.start_point[0]
330+
331+
# Stop merging if there is an empty line between comments
332+
if curr_start_line > last_end_line + 1:
333+
break
334+
335+
merged_texts.append(text)
336+
last_node = n
337+
idx += 1
338+
339+
return "\n".join(merged_texts), idx
340+
341+
@staticmethod
342+
def is_comment_alone_on_line(node: Any) -> bool:
343+
"""
344+
Return True if the comment node is the only thing on its line (ignoring whitespace).
345+
"""
346+
347+
if node.type != "comment":
348+
return False
349+
350+
parent = node.parent
351+
assert parent is not None
352+
353+
comment_line = node.start_point[0]
354+
355+
for sibling in parent.children:
356+
if sibling is node:
357+
continue
358+
start_line = sibling.start_point[0]
359+
end_line = sibling.end_point[0]
360+
361+
# If sibling shares the same line as comment
362+
if start_line <= comment_line <= end_line:
363+
# If it's not a comment (code, punctuation, etc.)
364+
if sibling.type != "comment":
365+
return False
366+
367+
return True

strictdoc/export/html/templates/screens/source_file_view/main.jinja

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282

8383
{#-- decide closer candidate: explicit end vs implicit close --#}
8484
{%- set marker_is_end = (is_marker and line.is_range_marker() and line.is_end()) -%}
85-
{%- set implicit_close = (is_markup and ns.prev_line != none) -%}
85+
{%- set implicit_close = (is_markup and ns.prev_line != none and ns.prev_line.ng_range_line_end == loop.index) -%}
8686

8787
{%- if marker_is_end -%}
8888
{%- set range_closer_line = line -%}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
def hello_world():
2+
# @relation(REQ-001, scope=line)
3+
print("Line marker") # noqa: T201
4+
5+
# @relation(REQ-001, scope=range_start)
6+
print("ignored hello world") # noqa: T201
7+
print("ignored hello world") # noqa: T201
8+
# @relation(REQ-001, scope=range_end)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[DOCUMENT]
2+
TITLE: Hello world doc
3+
4+
[REQUIREMENT]
5+
UID: REQ-001
6+
TITLE: Requirement Title
7+
STATEMENT: Requirement Statement
8+
RELATIONS:
9+
- TYPE: File
10+
VALUE: file.py
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[project]
2+
3+
features = [
4+
"REQUIREMENT_TO_SOURCE_TRACEABILITY",
5+
"SOURCE_FILE_LANGUAGE_PARSERS",
6+
]
7+
8+
exclude_source_paths = [
9+
"test.itest",
10+
]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# @relation(SDOC-SRS-124, scope=file)
2+
3+
RUN: %strictdoc export %S --output-dir %T | filecheck %s --dump-input=fail
4+
CHECK: Published: Hello world doc
5+
6+
RUN: %check_exists --file "%T/html/_source_files/file.py.html"
7+
8+
RUN: %cat %T/html/_source_files/file.py.html | filecheck %s --dump-input=fail --check-prefix CHECK-SOURCE-FILE
9+
CHECK-SOURCE-FILE: [ 2-3 ]
10+
CHECK-SOURCE-FILE: [ 5-8 ]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# ruff: noqa
2+
3+
def hello_world():
4+
# @relation(
5+
# REQ-001,
6+
# REQ-002,
7+
# REQ-003,
8+
# scope=line
9+
# )
10+
print("Line marker") # noqa: T201
11+
12+
# @relation(REQ-001, scope=range_start)
13+
print("ignored hello world") # noqa: T201
14+
print("ignored hello world") # noqa: T201
15+
# @relation(REQ-001, scope=range_end)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[DOCUMENT]
2+
TITLE: Hello world doc
3+
4+
[REQUIREMENT]
5+
UID: REQ-001
6+
TITLE: Requirement Title #1
7+
STATEMENT: Requirement Statement #1
8+
RELATIONS:
9+
- TYPE: File
10+
VALUE: file.py
11+
12+
[REQUIREMENT]
13+
UID: REQ-002
14+
TITLE: Requirement Title #2
15+
STATEMENT: Requirement Statement #2
16+
17+
[REQUIREMENT]
18+
UID: REQ-003
19+
TITLE: Requirement Title #3
20+
STATEMENT: Requirement Statement #3
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[project]
2+
3+
features = [
4+
"REQUIREMENT_TO_SOURCE_TRACEABILITY",
5+
"SOURCE_FILE_LANGUAGE_PARSERS",
6+
]
7+
8+
exclude_source_paths = [
9+
"test.itest",
10+
]

0 commit comments

Comments
 (0)