Path:
strictdoc/backend/sdoc_source_code/reader_rust.py
Lines:
729
Non-empty lines:
670
Non-empty lines covered with requirements:
670 / 670 (100.0%)
Functions:
21
Functions covered by requirements:
21 / 21 (100.0%)
1
"""2
@relation(SDOC-SRS-142, scope=file)3
"""4
5
from enum import IntEnum
6
from functools import lru_cache
7
from pathlib import Path, PurePath
8
from typing import Optional, cast
9
10
import toml
11
import tree_sitter_rust as ts_rust
12
from tree_sitter import Language, Node, Parser, Query, QueryCursor
13
14
from strictdoc.backend.sdoc_source_code.constants import FunctionAttribute
15
from strictdoc.backend.sdoc_source_code.marker_parser import MarkerParser
16
from strictdoc.backend.sdoc_source_code.models.language import LanguageItem
17
from strictdoc.backend.sdoc_source_code.models.language_item_marker import (
18
LanguageItemMarker,
19
RangeMarkerType,
20
)21
from strictdoc.backend.sdoc_source_code.models.line_marker import LineMarker
22
from strictdoc.backend.sdoc_source_code.models.range_marker import (
23
RangeMarker,
24
)25
from strictdoc.backend.sdoc_source_code.models.source_file_info import (
26
RelationMarkerType,
27
SourceFileTraceabilityInfo,
28
)29
from strictdoc.backend.sdoc_source_code.models.source_location import ByteRange
30
from strictdoc.backend.sdoc_source_code.parse_context import ParseContext
31
from strictdoc.backend.sdoc_source_code.processors.general_language_marker_processors import (
32
language_item_marker_processor,
33
line_marker_processor,
34
range_marker_processor,
35
source_file_traceability_info_processor,
36
)37
from strictdoc.helpers.cast import assert_cast
38
from strictdoc.helpers.file_stats import SourceFileStats
39
from strictdoc.helpers.file_system import (
40
file_open_read_bytes,
41
file_open_read_utf8,
42
)43
- "3.1.14. Valid positions of doc comments" (REQUIREMENT)
- "3.1.8. File, line and range markers" (REQUIREMENT)
- "3.1.10. Forward relations to Rust items" (REQUIREMENT)
44
# @relation(SDOC-LLR-177, SDOC-LLR-171, SDOC-LLR-173, scope=line)45
TS_QUERY = """
46
; Query 0: Outer doc attribute, line doc, or block doc in allowed positions47
(48
[49
(attribute_item50
(attribute51
(identifier) @_attribute_id (#eq? @_attribute_id "doc")52
value: (string_literal (string_content) @doc.comment)))+53
(line_comment54
outer: (outer_doc_comment_marker)55
doc: (doc_comment) @doc.comment)+56
(block_comment57
outer: (outer_doc_comment_marker)58
doc: (doc_comment) @doc.comment)59
]60
.61
(attribute_item)*62
.63
[64
; any identifiable item, most notably functions65
(_ name: [(identifier)(field_identifier)(type_identifier)(lifetime)] @doc.item_identifier)66
67
; impl MyStruct68
(impl_item type: (type_identifier) @doc.item_identifier)69
70
; extern "C"71
(foreign_mod_item (extern_modifier) @doc.item_identifier)72
73
; match arm74
(match_arm (match_pattern) @doc.item_identifier)75
76
; assignment inside struct initializer77
(field_initializer field: (field_identifier) @doc.item_identifier)78
79
; Statement like 1;80
(expression_statement) @doc.item_identifier81
82
; Expression like x + y83
(binary_expression) @doc.item_identifier84
85
; Expression like (x + y)86
(parenthesized_expression) @doc.item_identifier87
88
; Named "type" field of any enclosing node (usually body), e.g. type within tuple struct.89
type: (_)90
] @doc.item91
)92
93
; Query 1: Inner doc attribute, line doc or block doc in allowed positions.94
; Note: We have to repeat the identical inner pattern, alternations don't help here.95
; See https://github.com/tree-sitter/tree-sitter/issues/3480.96
[97
(function_item98
name: (identifier) @doc.item_identifier99
body: (block100
[101
(inner_attribute_item102
(attribute103
(identifier) @_attribute_id (#eq? @_attribute_id "doc")104
value: (string_literal (string_content) @doc.comment)))+105
(line_comment106
inner: (inner_doc_comment_marker)107
doc: (doc_comment) @doc.comment)+108
(block_comment109
inner: (inner_doc_comment_marker)110
doc: (doc_comment) @doc.comment)111
]112
)113
)114
(mod_item115
name: (identifier) @doc.item_identifier116
body: (declaration_list117
[118
(inner_attribute_item119
(attribute120
(identifier) @_attribute_id (#eq? @_attribute_id "doc")121
value: (string_literal (string_content) @doc.comment)))+122
(line_comment123
inner: (inner_doc_comment_marker)124
doc: (doc_comment) @doc.comment)+125
(block_comment126
inner: (inner_doc_comment_marker)127
doc: (doc_comment) @doc.comment)128
]129
)130
)131
(impl_item132
type: (type_identifier) @doc.item_identifier133
body: (declaration_list134
[135
(inner_attribute_item136
(attribute137
(identifier) @_attribute_id (#eq? @_attribute_id "doc")138
value: (string_literal (string_content) @doc.comment)))+139
(line_comment140
inner: (inner_doc_comment_marker)141
doc: (doc_comment) @doc.comment)+142
(block_comment143
inner: (inner_doc_comment_marker)144
doc: (doc_comment) @doc.comment)145
]146
)147
)148
(foreign_mod_item (extern_modifier) @doc.item_identifier149
body: (declaration_list150
[151
(inner_attribute_item152
(attribute153
(identifier) @_attribute_id (#eq? @_attribute_id "doc")154
value: (string_literal (string_content) @doc.comment)))+155
(line_comment156
inner: (inner_doc_comment_marker)157
doc: (doc_comment) @doc.comment)+158
(block_comment159
inner: (inner_doc_comment_marker)160
doc: (doc_comment) @doc.comment)161
]162
)163
)164
] @doc.item165
166
; Query 2: Inner line or block doc comment of file-level module.167
(source_file168
[169
(line_comment170
inner: (inner_doc_comment_marker)171
doc: (doc_comment) @doc.comment)+172
(block_comment173
inner: (inner_doc_comment_marker)174
doc: (doc_comment) @doc.comment)175
]176
) @doc.item177
178
; Query 3: normal line or block comment179
[(line_comment !doc)+180
(block_comment !doc)] @normal_comment181
182
; Query 4: Identifiable items. Those where it's clear how to link by forward relations.183
[184
(const_item name: (identifier) @doc.item_identifier) @doc.item185
(enum_item name: (type_identifier) @doc.item_identifier) @doc.item186
(function_item name: (identifier) @doc.item_identifier) @doc.item187
(mod_item name: (identifier) @doc.item_identifier) @doc.item188
(static_item name: (identifier) @doc.item_identifier) @doc.item189
(struct_item name: (type_identifier) @doc.item_identifier) @doc.item190
(trait_item name: (type_identifier) @doc.item_identifier) @doc.item191
(type_item name: (type_identifier) @doc.item_identifier) @doc.item192
(union_item name: (type_identifier) @doc.item_identifier) @doc.item193
]194
"""195
196
197
class RustTsQuery(IntEnum):
198
"""Give the queries from TS_QUERY a friendly name."""
199
200
OUTER_DOC_COMMENT = 0
201
INNER_DOC_COMMENT = 1
202
INNER_DOC_COMMENT_FILEMODULE = 2
203
NORMAL_COMMENT = 3
204
IDENTIFIABLE_ITEM = 4
205
206
207
@lru_cache(maxsize=None)
208
def rust_crate_root_and_name(directory: str) -> Optional[tuple[str, str]]:
209
"""
210
``(crate_root, package_name)`` of the nearest ``Cargo.toml`` with a211
``[package]`` at or above ``directory``, or ``None`` if there is none.212
``lru_cache`` memoizes the result per directory, so the many files of one213
crate cost a single manifest read.214
"""215
for current in (Path(directory), *Path(directory).parents):
216
manifest = current / "Cargo.toml"
217
if not manifest.is_file():
218
continue219
try:
220
with file_open_read_utf8(str(manifest)) as manifest_file:
221
package = toml.loads(manifest_file.read()).get("package")
222
except (OSError, toml.TomlDecodeError):
223
continue224
if isinstance(package, dict):
225
name = package.get("name")
226
if isinstance(name, str):
227
return str(current), name
228
return None
229
230
231
def rust_module_segments_within_crate(rel_parts: tuple[str, ...]) -> list[str]:
232
"""
233
Module path of a source file *within its crate*, from the file's path234
relative to the crate root split into ``rel_parts``. Applies Rust's235
file<->module convention (the reader sees one file, so it cannot follow236
``mod`` declarations)::237
238
src/lib.rs | src/main.rs -> [] (the crate root)239
src/model.rs -> ["model"]240
src/model/mod.rs -> ["model"]241
src/a/b/c.rs -> ["a", "b", "c"]242
tests/it.rs -> ["it"] (integration target)243
tests/it/helper.rs -> ["it", "helper"]244
245
The module path is taken from the file's location, so ``#[path = "..."]``246
overrides and non-default ``[lib]`` / ``[[bin]]`` target paths are not247
supported.248
"""249
250
def module_name(file: str) -> str:
251
return file[:-3] if file.endswith(".rs") else file
252
253
if not rel_parts:
254
return []
255
target_dir, inner = rel_parts[0], list(rel_parts[1:])
256
if not inner:
257
stem = module_name(target_dir)
258
return [] if stem in ("lib", "main") else [stem]
259
*dirs, leaf = inner
260
stem = module_name(leaf)
261
if target_dir == "src" and not dirs and stem in ("lib", "main"):
262
return []
263
if stem in ("mod", "main"):
264
return dirs
265
return [*dirs, stem]
266
267
268
def rust_canonical_crate_segments(full_path: Optional[str]) -> list[str]:
269
"""
270
Canonical-path prefix shared by every item in a Rust file: the crate name271
(the ``[package]`` of the nearest ``Cargo.toml``, or the file stem when272
there is none) followed by the file's module path within the crate.273
"""274
if not full_path:
275
return []
276
path = PurePath(full_path)
277
crate = rust_crate_root_and_name(str(path.parent))
278
if crate is None:
279
return [path.stem]
280
crate_root, crate_name = crate
281
try:
282
rel_parts = path.relative_to(crate_root).parts
283
except ValueError:
284
return [path.stem]
285
return [crate_name, *rust_module_segments_within_crate(rel_parts)]
286
287
- "3.1.12. Collapse doc comments" (REQUIREMENT)
288
def comments_text_from_comment_nodes(comments: list[Node]) -> str:
289
"""
290
Join multiple comment nodes into one multi-line string.291
@relation(SDOC-LLR-175, scope=function)292
"""293
comment_text = assert_cast(comments[0].text, bytes).decode("utf-8")
294
last_row = comments[0].start_point.row
295
for comment_part in comments[1:]:
296
new_lines = comment_part.start_point.row - last_row
297
last_row = comment_part.start_point.row
298
comment_text += "\n" * new_lines + assert_cast(
299
comment_part.text, bytes
300
).decode("utf-8")
301
return comment_text
302
303
304
def item_definition_line_begin(item: Node) -> int:
305
"""
306
First 1-based line of ``item``'s definition, including the leading outer307
attributes (``#[test]``, ``#[cfg(...)]``) and doc/line comments above it,308
which tree-sitter models as siblings rather than part of the item node.309
"""310
line_begin_0_based = item.start_point[0]
311
sibling = item.prev_sibling
312
while sibling is not None and sibling.type in (
313
"attribute_item",
314
"line_comment",
315
"block_comment",
316
):317
# A line comment node extends to the start of the following line, so its318
# last line of content is its start row; attributes and block comments319
# end where their end point is.320
sibling_content_end = (
321
sibling.start_point[0]
322
if sibling.type == "line_comment"
323
else sibling.end_point[0]
324
)325
# Stop once a blank line separates the sibling from the header: such a326
# comment documents something else, not this definition.327
if sibling_content_end != line_begin_0_based - 1:
328
break329
line_begin_0_based = sibling.start_point[0]
330
sibling = sibling.prev_sibling
331
return line_begin_0_based + 1
332
333
334
def special_description(item: Node, identifier_text: str) -> Optional[str]:
335
"""
336
Make a description for language constructs that are not functions.337
338
The default Function description assumes the object actually represents a function.339
However, the Rust reader reuses Function to represent many different Rust specific object types.340
We have to give them a suitable Rust specific description.341
"""342
if item.type == "associated_type":
343
return f"associated type {identifier_text}"
344
elif item.type in ("binary_expression", "parenthesized_expression"):
345
return f"expression {identifier_text}"
346
elif item.type == "const_item":
347
return f"const {identifier_text}"
348
elif item.type == "const_parameter":
349
return f"const parameter {identifier_text}"
350
elif item.type == "function_item":
351
return f"fn {identifier_text}()"
352
elif item.type == "enum_item":
353
return f"enum {identifier_text}"
354
elif item.type == "enum_variant":
355
return f"enum variant {identifier_text}"
356
elif item.type == "expression_statement":
357
return f"statement {identifier_text}"
358
elif item.type == "extern_crate_declaration":
359
return f"crate {identifier_text}"
360
elif item.type == "field_declaration":
361
return f"field {identifier_text}"
362
elif item.type == "field_initializer":
363
return f"field initializer {identifier_text}"
364
elif item.type == "foreign_mod_item":
365
return f"foreign module {identifier_text}"
366
elif item.type == "impl_item":
367
return f"impl {identifier_text}"
368
elif item.type == "match_arm":
369
return f"match arm {identifier_text}"
370
elif item.type == "macro_definition":
371
return f"macro {identifier_text}"
372
elif item.type == "mod_item":
373
return f"module {identifier_text}"
374
elif item.type == "lifetime_parameter":
375
return f"lifetime {identifier_text}"
376
elif item.type == "static_item":
377
return f"static {identifier_text}"
378
elif item.type == "struct_item":
379
return f"struct {identifier_text}"
380
elif item.type == "trait_item":
381
return f"trait {identifier_text}"
382
elif item.type == "type_item":
383
return f"type {identifier_text}"
384
elif item.type == "type_parameter":
385
return f"type parameter {identifier_text}"
386
elif item.type == "union_item":
387
return f"union {identifier_text}"
388
elif item.type in ("primitive_type", "type_identifier"):
389
return f"type {identifier_text}"
390
return None
391
392
393
class SourceFileTraceabilityReader_Rust:
394
@staticmethod395
def supported_elements() -> list[str]:
396
return []
397
398
def __init__(self, custom_tags: Optional[set[str]] = None) -> None:
399
self.custom_tags: Optional[set[str]] = custom_tags
400
401
def read(
402
self,
403
input_buffer: bytes,
404
file_path: Optional[str] = None,
405
) -> SourceFileTraceabilityInfo:
406
file_stats = SourceFileStats.create(input_buffer)
407
parse_context = ParseContext(file_path, file_stats)
408
traceability_info = SourceFileTraceabilityInfo([])
409
parser = ParserRun(
410
input_buffer, parse_context, traceability_info, self.custom_tags
411
)412
parser()
413
source_file_traceability_info_processor(
414
traceability_info, parse_context
415
)416
return traceability_info
417
418
def read_from_file(self, file_path: str) -> SourceFileTraceabilityInfo:
419
"""
420
Generate the source file traceability info for one particular Rust file.421
422
The created SourceFileTraceabilityInfo is filled partially local information:423
- functions: Markers are associated, but only those resulting from local markers.424
- markers: Markers that stem from markup in this file.425
- ng_map_reqs_to_markers: Mapping of requirement IDs to Marker objects for markers directly defined in the source file.426
"""427
with file_open_read_bytes(file_path) as file:
428
sdoc_content = file.read()
429
sdoc = self.read(sdoc_content, file_path=file_path)
430
return sdoc
431
432
433
class ParserRun:
434
def __init__(
435
self,
436
input_buffer: bytes,
437
parse_context: ParseContext,
438
traceability_info: SourceFileTraceabilityInfo,
439
custom_tags: Optional[set[str]],
440
):441
rust_language = Language(ts_rust.language())
442
self.parser = Parser(rust_language) # type: ignore[call-arg, unused-ignore]
443
self.TS_QUERY = Query(rust_language, TS_QUERY)
444
self.input_buffer: bytes = input_buffer
445
self.parse_context = parse_context
446
self.traceability_info = traceability_info
447
self.custom_tags: Optional[set[str]] = custom_tags
448
449
def __call__(self) -> None:
450
tree = self.parser.parse(self.input_buffer)
451
cursor = QueryCursor(self.TS_QUERY)
452
matches = cursor.matches(tree.root_node)
453
454
seen_nodes = set()
455
deferred_matches = []
456
for query_index, captures in matches:
457
if query_index == RustTsQuery.IDENTIFIABLE_ITEM:
458
# The query for identifiable items overlaps with comment based queries. Move results for identifiable459
# items last, so that a result can be skipped if a LanguageItem was already created.460
deferred_matches.append(captures)
461
elif query_index in (
462
RustTsQuery.OUTER_DOC_COMMENT,
463
RustTsQuery.INNER_DOC_COMMENT,
464
RustTsQuery.INNER_DOC_COMMENT_FILEMODULE,
465
):466
assert len(captures["doc.item"]) == 1
467
item = captures["doc.item"][0]
468
doc_comment = captures["doc.comment"]
469
if (
470
item.type == "source_file"
471
and "doc.item_identifier" not in captures
472
):473
self._process_anonymous_module_comment(
474
doc_comment,
475
item,
476
)477
else:
478
if "doc.item_identifier" in captures:
479
# doc comment on named item480
assert len(captures["doc.item_identifier"]) == 1
481
identifier = assert_cast(
482
captures["doc.item_identifier"][0].text, bytes
483
).decode()
484
else:
485
# doc comment on anonymous item486
identifier = assert_cast(item.text, bytes).decode()
487
self._process_doc_comment(
488
doc_comment,
489
item,
490
identifier,
491
)492
seen_nodes.add(item.id)
493
elif query_index == RustTsQuery.NORMAL_COMMENT:
494
self._process_normal_comment(captures["normal_comment"])
495
496
for captures in deferred_matches:
497
assert len(captures["doc.item"]) == 1
498
assert len(captures["doc.item_identifier"]) == 1
499
item = captures["doc.item"][0]
500
if item.id not in seen_nodes:
501
identifier = assert_cast(
502
captures["doc.item_identifier"][0].text, bytes
503
).decode()
504
self._process_item_for_forward_relation(item, identifier)
505
- "3.1.1. Auto-scoped relation markers in Rust docs" (REQUIREMENT)
- "3.1.9. Source nodes from doc comments" (REQUIREMENT)
506
def _process_anonymous_module_comment(
507
self, comments: list[Node], module: Node
508
) -> None:
509
"""
510
Create marker, item and source nodes for file-level module from tree-sitter doc comment nodes.511
@relation(SDOC-LLR-164, SDOC-LLR-172, scope=function)512
"""513
comment_text = comments_text_from_comment_nodes(comments)
514
source_node = MarkerParser.parse(
515
input_string=comment_text,
516
line_start=1,
517
line_end=self.parse_context.file_stats.lines_total,
518
comment_line_start=module.start_point.row + 1,
519
comment_byte_range=ByteRange.create_from_ts_nodes(
520
comments[0], comments[-1]
521
),522
custom_tags=self.custom_tags,
523
default_scope="file",
524
)525
for marker_ in source_node.markers:
526
if not isinstance(marker_, LanguageItemMarker):
527
continue528
# At the top level, only accept the scope=file markers.529
# Everything else will be handled by functions and classes.530
if marker_.scope != RangeMarkerType.FILE:
531
print( # noqa: T201
532
"warning: comment to top-level module is not scope=file, ignoring"533
)534
continue535
language_item_marker_processor(marker_, self.parse_context)
536
self.traceability_info.markers.append(marker_)
537
- "3.1.1. Auto-scoped relation markers in Rust docs" (REQUIREMENT)
- "3.1.9. Source nodes from doc comments" (REQUIREMENT)
538
def _process_doc_comment(
539
self,
540
comments: list[Node],
541
item: Node,
542
identifier_text: str,
543
) -> None:
544
"""
545
Create markers, items and source nodes from tree-sitter doc comment nodes.546
@relation(SDOC-LLR-164, SDOC-LLR-172, scope=function)547
"""548
assert len(comments) >= 1
549
comment_text = comments_text_from_comment_nodes(comments)
550
line_start_0_based = min(
551
item.start_point[0], comments[0].start_point[0]
552
)553
line_end_0_based = max(item.end_point[0], comments[-1].end_point[0])
554
source_node = MarkerParser.parse(
555
input_string=comment_text,
556
line_start=line_start_0_based + 1,
557
line_end=line_end_0_based + 1
558
if self.input_buffer[-1] == 10
559
else line_end_0_based,
560
comment_line_start=comments[0].start_point[0] + 1,
561
comment_byte_range=ByteRange.create_from_ts_nodes(
562
comments[0], comments[-1]
563
),564
custom_tags=self.custom_tags,
565
entity_name=identifier_text,
566
default_scope="function",
567
)568
569
function_markers: list[LanguageItemMarker] = []
570
for marker_ in source_node.markers:
571
if isinstance(marker_, LanguageItemMarker) and (
572
language_item_marker_ := marker_
573
):574
if (
575
description := special_description(item, identifier_text)
576
) is not None:
577
language_item_marker_.set_description(description)
578
579
# adds marker to context, and connects context requirements with marker580
language_item_marker_processor(
581
language_item_marker_, self.parse_context
582
)583
self.traceability_info.markers.append(language_item_marker_)
584
function_markers.append(marker_)
585
586
name = self.canonical_path(item.parent, identifier_text)
587
new_function_for_rust_item = LanguageItem(
588
parent=self.traceability_info,
589
name=name,
590
display_name=name,
591
line_begin=item_definition_line_begin(item),
592
line_end=item.end_point[0] + 1,
593
code_byte_range=ByteRange.create_from_ts_node(item),
594
child_functions=[],
595
markers=function_markers,
596
attributes={FunctionAttribute.DEFINITION},
597
)598
if len(source_node.fields) > 0:
599
source_node.function = new_function_for_rust_item
600
self.traceability_info.source_nodes.append(source_node)
601
self.traceability_info.functions.append(new_function_for_rust_item)
602
# list is invariant, so list[LanguageItemMarker] is not a603
# list[RelationMarkerType] even though every element is one. The widen604
# is sound; cast it for the map, which holds the wider type.605
self.traceability_info.ng_map_names_to_markers[identifier_text] = cast(
606
list[RelationMarkerType], function_markers
607
)608
- "3.1.8. File, line and range markers" (REQUIREMENT)
609
def _process_normal_comment(self, comments: list[Node]) -> None:
610
"""
611
Create markers and items from tree-sitter normal comment nodes.612
@relation(SDOC-LLR-171, scope=function)613
"""614
comment_text = comments_text_from_comment_nodes(comments)
615
line_start_0_based = comments[0].start_point.row
616
line_end_0_based = comments[-1].end_point.row
617
source_node = MarkerParser.parse(
618
input_string=comment_text,
619
line_start=line_start_0_based + 1,
620
line_end=line_end_0_based + 1,
621
comment_line_start=line_start_0_based + 1,
622
comment_byte_range=ByteRange.create_from_ts_nodes(
623
comments[0], comments[-1]
624
),625
)626
for marker_ in source_node.markers:
627
if (
628
isinstance(marker_, LanguageItemMarker)
629
and (marker_.scope is RangeMarkerType.FILE)
630
and (language_item_marker := marker_)
631
):632
language_item_marker.ng_range_line_begin = 1
633
language_item_marker.ng_range_line_end = (
634
self.parse_context.file_stats.lines_total
635
)636
language_item_marker_processor(
637
language_item_marker, self.parse_context
638
)639
elif isinstance(marker_, RangeMarker) and (range_marker := marker_):
640
range_marker_processor(range_marker, self.parse_context)
641
elif isinstance(marker_, LineMarker) and (line_marker := marker_):
642
line_marker_processor(line_marker, self.parse_context)
643
else:
644
print( # noqa: T201
645
"warning: Ignoring @relation. Only scope=file|line|range_start is supported in regular "646
"Rust comments. Use doc comments otherwise."647
)648
- "3.1.10. Forward relations to Rust items" (REQUIREMENT)
649
def _process_item_for_forward_relation(
650
self, item: Node, identifier: str
651
) -> None:
652
"""
653
Create item objects from tree-sitter doc comment nodes to support forward relations.654
655
Corresponding markers will be created and resolved later by FileTraceabilityIndex,656
see validate_and_resolve.657
658
@relation(SDOC-LLR-173, scope=function)659
"""660
name = self.canonical_path(item.parent, identifier)
661
function = LanguageItem(
662
parent=self.traceability_info,
663
name=name,
664
display_name=name,
665
line_begin=item_definition_line_begin(item),
666
line_end=max(item.end_point[0] + 1, item.start_point[0] + 2),
667
code_byte_range=ByteRange.create_from_ts_node(item),
668
child_functions=[],
669
markers=[],
670
attributes={FunctionAttribute.DEFINITION},
671
)672
self.traceability_info.functions.append(function)
673
- "3.1.11. Forward relation by canonical path" (REQUIREMENT)
674
def canonical_path(
675
self, parent_scope: Optional[Node], item_path_segment: str
676
) -> str:
677
"""
678
Construct a canonical path in best-effort.679
@relation(SDOC-LLR-174, scope=function)680
"""681
cursor: Optional[Node] = parent_scope
682
683
if (
684
cursor is not None
685
and cursor.type == "declaration_list"
686
and cursor.parent is not None
687
and cursor.parent.type == "impl_item"
688
):689
cursor = cursor.parent
690
item_being_implemented = cursor.child_by_field_name("type")
691
assert item_being_implemented is not None
692
canonical_path_item_being_implemented = self.canonical_path(
693
cursor,
694
assert_cast(item_being_implemented.text, bytes).decode("utf-8"),
695
)696
impl_trait_node = cursor.child_by_field_name("trait")
697
if impl_trait_node is not None:
698
# rust-lang.org: For trait implementations, [the path prefix] is the canonical path of the item being699
# implemented followed by as followed by the canonical path to the trait all surrounded in angle (<>)700
# brackets.701
trait = self.canonical_path(
702
None,
703
assert_cast(impl_trait_node.text, bytes).decode("utf-8"),
704
)705
path_prefix = (
706
f"<{canonical_path_item_being_implemented} as {trait}>"
707
)708
else:
709
# rust-lang.org: For bare implementations, [the path prefix] is the canonical path of the item being710
# implemented surrounded by angle (<>) brackets.711
path_prefix = f"<{canonical_path_item_being_implemented}>"
712
else:
713
path_prefix_segments = []
714
while cursor is not None:
715
name_node = cursor.child_by_field_name("name")
716
if name_node is not None:
717
name = assert_cast(name_node.text, bytes).decode("utf-8")
718
path_prefix_segments.append(name)
719
cursor = cursor.parent
720
path_prefix_segments.extend(
721
reversed(
722
rust_canonical_crate_segments(self.parse_context.filename)
723
)724
)725
path_prefix = "::".join(reversed(path_prefix_segments))
726
727
# rust-lang.org: The canonical path is defined as a path prefix appended by the path segment the item itself728
# defines.729
return f"{path_prefix}::{item_path_segment}"