Skip to content

Commit 551bc61

Browse files
authored
Allow explicit product ID in traces (#64)
* Add test data for the Markdown requirements format * Allow traces to explicitly set the product id besides string literal for the requirement ID, traces now accept "{ id: "<requirement ID>", product_id: "<product ID>" }" * Adapt requirement trace tables It is now possible to explicitly set a product id for a requirement trace.
1 parent 57af8ae commit 551bc61

63 files changed

Lines changed: 1614 additions & 138 deletions

File tree

Some content is hidden

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

Cargo.lock

Lines changed: 385 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

langs/mantra-lang-tracing/src/collect/rust/mod.rs

Lines changed: 191 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ use mantra_schema::{
66
annotations::{
77
Annotations, CodeBlock, Element, ElementKind, Trace, TraceKind, TraceRelatedCodeVariant,
88
},
9-
requirements::ReqId,
9+
product::ProductId,
10+
requirements::{ReqId, RequirementPk},
1011
};
1112
use serde_json::Map;
1213
use tree_sitter::{Node, Parser, TreeCursor};
@@ -389,7 +390,7 @@ fn get_req_ids(
389390
content: &[u8],
390391
start_line: Line,
391392
fn_like: bool,
392-
) -> Result<Vec<ReqId>, anyhow::Error> {
393+
) -> Result<Vec<RequirementPk>, anyhow::Error> {
393394
let line = args_node.start_position().row + start_line.try_into().unwrap_or(0);
394395

395396
if args_node.has_error() {
@@ -399,29 +400,34 @@ fn get_req_ids(
399400
let mut ids = Vec::new();
400401

401402
let mut cursor = args_node.walk();
402-
let mut expected_kind = "(";
403+
let mut expected_kinds: &[&str] = &["("];
403404
let mut reached_end = false;
404405

405406
for child in args_node.children(&mut cursor) {
406-
if expected_kind == child.kind() {
407-
if expected_kind == "(" {
408-
expected_kind = "string_literal";
409-
} else if expected_kind == "string_literal"
407+
if expected_kinds.contains(&child.kind()) {
408+
if child.kind() == "token_tree" {
409+
let req_pk = get_req_pk(&child, content, start_line)?;
410+
ids.push(req_pk);
411+
expected_kinds = &[","];
412+
} else if child.kind() == "string_literal"
410413
&& let Some(content_node) = child.named_child(0)
411414
&& let Ok(id) = content_node.utf8_text(content)
412415
{
413-
ids.push(ReqId::from_str(id)?);
414-
expected_kind = ",";
416+
ids.push(RequirementPk {
417+
id: ReqId::from_str(id)?,
418+
product_id: None,
419+
});
420+
expected_kinds = &[","];
415421
} else {
416-
expected_kind = "string_literal";
422+
expected_kinds = &["string_literal", "token_tree"];
417423
}
418424
} else if child.kind() == ")" || (child.kind() == "=>" && fn_like) {
419425
// Note: ")" is end of a simple macro and "=>" marks start of code block in fn-like macros
420426
reached_end = true;
421427
break;
422428
} else {
423429
bail!(
424-
"Mantra trace at line '{line}' must only consist of comma separated string literals!"
430+
"Mantra trace at line '{line}' must only consist of comma separated string literals or objects with fields 'id' and 'product_id'!"
425431
);
426432
}
427433
}
@@ -433,6 +439,180 @@ fn get_req_ids(
433439
Ok(ids)
434440
}
435441

442+
fn get_req_pk(
443+
tree_node: &Node<'_>,
444+
content: &[u8],
445+
_start_line: Line,
446+
) -> Result<RequirementPk, anyhow::Error> {
447+
const ID_IDENT: &str = "id";
448+
const PRODUCT_ID_IDENT: &str = "product_id";
449+
450+
let mut id = None;
451+
let mut product_id = None;
452+
453+
let mut cursor = tree_node.walk();
454+
let mut children = tree_node.children(&mut cursor);
455+
let curly_open = children.next().ok_or(anyhow!(
456+
"Expected '{{' as start of a requirement trace using the explicit object notation."
457+
))?;
458+
459+
if curly_open.kind() != "{" {
460+
bail!("Expected '{{' as start of a requirement trace using the explicit object notation.")
461+
}
462+
463+
let first_field = children.next().ok_or(anyhow!(
464+
"Expected first field of a requirement trace using the explicit object notation."
465+
))?;
466+
let first_field_separator = children.next().ok_or(anyhow!(
467+
"Expected field separator ':' of a requirement trace using the explicit object notation."
468+
))?;
469+
470+
if first_field_separator.kind() != ":" {
471+
bail!(
472+
"Expected field separator ':' of a requirement trace using the explicit object notation."
473+
);
474+
}
475+
476+
let first_field_value = children.next().ok_or(anyhow!(
477+
"Expected field value of a requirement trace using the explicit object notation."
478+
))?;
479+
480+
let resolved_first_ident = if first_field.kind() == "identifier"
481+
&& let Ok(ident) = first_field.utf8_text(content)
482+
{
483+
ident
484+
} else if first_field.kind() == "string_literal"
485+
&& let Some(content_node) = first_field.named_child(0)
486+
&& let Ok(key) = content_node.utf8_text(content)
487+
{
488+
key
489+
} else {
490+
bail!(
491+
"Expected either '{}' or '{}' as keys",
492+
ID_IDENT,
493+
PRODUCT_ID_IDENT
494+
)
495+
};
496+
497+
if resolved_first_ident != ID_IDENT && resolved_first_ident != PRODUCT_ID_IDENT {
498+
bail!(
499+
"Found field '{}'. Allowed fields are '{}' and '{}'",
500+
resolved_first_ident,
501+
ID_IDENT,
502+
PRODUCT_ID_IDENT,
503+
);
504+
}
505+
506+
if first_field_value.kind() != "string_literal" {
507+
bail!(
508+
"Expected string literal as value for field '{}' of a requirement trace using the explicit object notation.",
509+
resolved_first_ident
510+
);
511+
}
512+
513+
let first_field_value_content = first_field_value
514+
.named_child(0)
515+
.ok_or(anyhow!("Expected string value"))?;
516+
let first_value = first_field_value_content.utf8_text(content)?;
517+
518+
if resolved_first_ident == ID_IDENT {
519+
id = Some(ReqId::from_str(first_value)?);
520+
} else if resolved_first_ident == PRODUCT_ID_IDENT {
521+
product_id = Some(ProductId::from_str(first_value)?);
522+
}
523+
524+
let mut next_child = children
525+
.next()
526+
.ok_or(anyhow!("Expected ',' or closing '}}'"))?;
527+
528+
if next_child.kind() == "," {
529+
next_child = children
530+
.next()
531+
.ok_or(anyhow!("Expected identifier or closing '}}'"))?;
532+
533+
if next_child.kind() == "identifier" || next_child.kind() == "string_literal" {
534+
let resolved_second_ident = if first_field.kind() == "identifier"
535+
&& let Ok(ident) = next_child.utf8_text(content)
536+
{
537+
ident
538+
} else if next_child.kind() == "string_literal"
539+
&& let Some(content_node) = next_child.named_child(0)
540+
&& let Ok(key) = content_node.utf8_text(content)
541+
{
542+
key
543+
} else {
544+
bail!("Failed to extract the second key");
545+
};
546+
547+
let second_field_separator = children.next().ok_or(anyhow!(
548+
"Expected field separator ':' of a requirement trace using the explicit object notation."
549+
))?;
550+
551+
if second_field_separator.kind() != ":" {
552+
bail!(
553+
"Expected field separator ':' of a requirement trace using the explicit object notation."
554+
);
555+
}
556+
557+
let second_field_value = children.next().ok_or(anyhow!(
558+
"Expected field value of a requirement trace using the explicit object notation."
559+
))?;
560+
561+
if second_field_value.kind() != "string_literal" {
562+
bail!(
563+
"Expected string literal as value for field '{}' of a requirement trace using the explicit object notation.",
564+
resolved_second_ident
565+
);
566+
}
567+
568+
let second_field_value_content = second_field_value
569+
.named_child(0)
570+
.ok_or(anyhow!("Expected string value"))?;
571+
let second_value = second_field_value_content.utf8_text(content)?;
572+
573+
if resolved_second_ident == ID_IDENT {
574+
if id.is_some() {
575+
bail!("Duplicate field '{}'", ID_IDENT);
576+
} else {
577+
id = Some(ReqId::from_str(second_value)?);
578+
}
579+
} else if resolved_second_ident == PRODUCT_ID_IDENT {
580+
if product_id.is_some() {
581+
bail!("Duplicate field '{}'", PRODUCT_ID_IDENT);
582+
} else {
583+
product_id = Some(ProductId::from_str(second_value)?);
584+
}
585+
}
586+
587+
next_child = children
588+
.next()
589+
.ok_or(anyhow!("Expected either ',' or '}}'"))?;
590+
591+
if next_child.kind() == "," {
592+
next_child = children.next().ok_or(anyhow!("Expected '}}'"))?;
593+
}
594+
}
595+
}
596+
597+
if next_child.kind() != "}" {
598+
bail!("Expected closing '}}'");
599+
}
600+
601+
if children.next().is_some() {
602+
bail!("Expected no more tokens after closing '}}'");
603+
}
604+
605+
Ok(RequirementPk {
606+
id: id.ok_or_else(|| {
607+
anyhow!(
608+
"Missing field '{}' for the explicit requirement trace object notation",
609+
ID_IDENT
610+
)
611+
})?,
612+
product_id,
613+
})
614+
}
615+
436616
fn get_attrb_macro_trace_kind(node: &Node<'_>, content: &[u8]) -> Option<TraceKind> {
437617
let ident_node = get_ident_node(node)?;
438618
let macro_name = ident_node.utf8_text(content).ok()?;

langs/mantra-lang-tracing/src/collect/rust/snapshots/mantra_lang_tracing__collect__rust__tests__assert_eq_fn_like.snap

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ Annotations(
66
traces: [
77
Trace(
88
ids: [
9-
"ID1.assert",
9+
RequirementPk(
10+
id: "ID1.assert",
11+
product_id: None,
12+
),
1013
],
1114
line: 3,
1215
related_code: Some(CodeBlock(

langs/mantra-lang-tracing/src/collect/rust/snapshots/mantra_lang_tracing__collect__rust__tests__assert_fn_like.snap

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ Annotations(
66
traces: [
77
Trace(
88
ids: [
9-
"ID1.assert",
9+
RequirementPk(
10+
id: "ID1.assert",
11+
product_id: None,
12+
),
1013
],
1114
line: 3,
1215
related_code: Some(CodeBlock(

langs/mantra-lang-tracing/src/collect/rust/snapshots/mantra_lang_tracing__collect__rust__tests__assert_ne_fn_like.snap

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ Annotations(
66
traces: [
77
Trace(
88
ids: [
9-
"ID1.assert",
9+
RequirementPk(
10+
id: "ID1.assert",
11+
product_id: None,
12+
),
1013
],
1114
line: 3,
1215
related_code: Some(CodeBlock(

langs/mantra-lang-tracing/src/collect/rust/snapshots/mantra_lang_tracing__collect__rust__tests__attrb.snap

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ Annotations(
66
traces: [
77
Trace(
88
ids: [
9-
"ID1",
9+
RequirementPk(
10+
id: "ID1",
11+
product_id: None,
12+
),
1013
],
1114
line: 2,
1215
related_code: Some(3),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
source: langs/mantra-lang-tracing/src/collect/rust/tests.rs
3+
info: "\n #[req({ id: \"ID1\"})]\n fn foo() {}\n "
4+
---
5+
Annotations(
6+
traces: [
7+
Trace(
8+
ids: [
9+
RequirementPk(
10+
id: "ID1",
11+
product_id: None,
12+
),
13+
],
14+
line: 2,
15+
related_code: Some(3),
16+
kind: satisfies,
17+
properties: None,
18+
),
19+
],
20+
elements: [
21+
Element(
22+
ident: None,
23+
name: "foo",
24+
definition_line: 3,
25+
span: LineSpan(
26+
start: 2,
27+
end: 3,
28+
),
29+
kind: function,
30+
content_hash: Some("ba55caeb8853837f47a9cc5dfa7605ab642d2b542e103cda97646ad52ba38335"),
31+
),
32+
],
33+
coverage_excludes: [],
34+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
source: langs/mantra-lang-tracing/src/collect/rust/tests.rs
3+
info: "\n #[req({ id: \"ID1\",})]\n fn foo() {}\n "
4+
---
5+
Annotations(
6+
traces: [
7+
Trace(
8+
ids: [
9+
RequirementPk(
10+
id: "ID1",
11+
product_id: None,
12+
),
13+
],
14+
line: 2,
15+
related_code: Some(3),
16+
kind: satisfies,
17+
properties: None,
18+
),
19+
],
20+
elements: [
21+
Element(
22+
ident: None,
23+
name: "foo",
24+
definition_line: 3,
25+
span: LineSpan(
26+
start: 2,
27+
end: 3,
28+
),
29+
kind: function,
30+
content_hash: Some("6b1f0654e68edefb11f874d1fec72c3bd749dcf6298ee7da64447f2d0598db21"),
31+
),
32+
],
33+
coverage_excludes: [],
34+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
source: langs/mantra-lang-tracing/src/collect/rust/tests.rs
3+
info: "\n #[req({ id: \"ID1\", product_id: \"product-id\"})]\n fn foo() {}\n "
4+
---
5+
Annotations(
6+
traces: [
7+
Trace(
8+
ids: [
9+
RequirementPk(
10+
id: "ID1",
11+
product_id: Some("product-id"),
12+
),
13+
],
14+
line: 2,
15+
related_code: Some(3),
16+
kind: satisfies,
17+
properties: None,
18+
),
19+
],
20+
elements: [
21+
Element(
22+
ident: None,
23+
name: "foo",
24+
definition_line: 3,
25+
span: LineSpan(
26+
start: 2,
27+
end: 3,
28+
),
29+
kind: function,
30+
content_hash: Some("7f3a0b5acca5b17a8a75c0d0f8bf40277622475977261211aeceebbcfe2018bf"),
31+
),
32+
],
33+
coverage_excludes: [],
34+
)

0 commit comments

Comments
 (0)