Skip to content

Commit 0a25fd4

Browse files
Copilotjongalloway
andcommitted
fix: address PR review feedback on class diagram parser
Co-authored-by: jongalloway <68539+jongalloway@users.noreply.github.com>
1 parent a77267e commit 0a25fd4

2 files changed

Lines changed: 54 additions & 25 deletions

File tree

src/DiagramForge/Parsers/Mermaid/MermaidClassDiagramParser.cs

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,22 @@ internal sealed class MermaidClassDiagramParser : IMermaidDiagramParser
77
{
88
// Relationship operators sorted so that longer tokens are tried first when positions tie.
99
// Each entry: (token, relationshipType, isReversed).
10-
// "isReversed" is not used for edge direction (we always keep source=left, target=right)
11-
// but is stored in metadata for potential renderer use.
10+
// isReversed=true means the written textual order is source-on-right / target-on-left,
11+
// so SourceId and TargetId must be swapped relative to the left/right parse order.
12+
// For example: "Animal <|-- Dog" → left=Animal, right=Dog, but Dog IS the child
13+
// (semantic source), so isReversed=true causes Edge(Dog, Animal).
1214
private static readonly (string Token, string RelType, bool IsReversed)[] RelOperators =
1315
[
14-
("<|--", "inheritance", false),
15-
("--|>", "inheritance", true),
16-
("<|..", "realization", false),
17-
("..|>", "realization", true),
16+
("<|--", "inheritance", true),
17+
("--|>", "inheritance", false),
18+
("<|..", "realization", true),
19+
("..|>", "realization", false),
1820
("*--", "composition", false),
1921
("--*", "composition", true),
2022
("o--", "aggregation", false),
2123
("--o", "aggregation", true),
22-
("<..", "dependency", false),
23-
("..>", "dependency", true),
24+
("<..", "dependency", true),
25+
("..>", "dependency", false),
2426
("<--", "association", true),
2527
("-->", "association", false),
2628
("..", "link", false),
@@ -83,7 +85,7 @@ Node GetOrCreateNode(string id)
8385

8486
// ── Explicit class declaration: "class ClassName" or
8587
// "class ClassName[\"label\"]" or "class ClassName {" ─────────
86-
if (line.StartsWith("class ", StringComparison.Ordinal))
88+
if (line.StartsWith("class ", StringComparison.OrdinalIgnoreCase))
8789
{
8890
var rest = line[6..].Trim();
8991
bool opensBrace = rest.EndsWith('{');
@@ -117,7 +119,7 @@ Node GetOrCreateNode(string id)
117119

118120
// ── Relationship line (try before colon-member to avoid false ────
119121
// positives on "ClassName : member" that looks like a target) ─
120-
if (TryParseRelationship(line, builder, GetOrCreateNode, hints))
122+
if (TryParseRelationship(line, builder, GetOrCreateNode))
121123
continue;
122124

123125
// ── Colon-member: "ClassName : memberDefinition" ─────────────────
@@ -211,8 +213,7 @@ private static bool TryParseColonMember(string line, Func<string, Node> getOrCre
211213
private static bool TryParseRelationship(
212214
string line,
213215
IDiagramSemanticModelBuilder builder,
214-
Func<string, Node> getOrCreate,
215-
LayoutHints hints)
216+
Func<string, Node> getOrCreate)
216217
{
217218
var found = FindRelationshipOp(line);
218219
if (found is null)
@@ -246,7 +247,13 @@ private static bool TryParseRelationship(
246247
getOrCreate(leftId);
247248
getOrCreate(rightId);
248249

249-
var edge = new Edge(leftId, rightId);
250+
// When isReversed=true the semantic "from" end is on the right side of the operator
251+
// (e.g. "Animal <|-- Dog": Dog IS the child/source, Animal is the parent/target).
252+
// Swap so that SourceId always represents the logical origin of the relationship.
253+
string sourceId = isReversed ? rightId : leftId;
254+
string targetId = isReversed ? leftId : rightId;
255+
256+
var edge = new Edge(sourceId, targetId);
250257
if (label is not null)
251258
edge.Label = new Label(label);
252259

@@ -292,8 +299,10 @@ private static ArrowHeadStyle MapArrowHead(string relType) =>
292299
relType switch
293300
{
294301
"association" or "dependency" => ArrowHeadStyle.Arrow,
295-
"composition" or "aggregation" => ArrowHeadStyle.Diamond,
296-
_ => ArrowHeadStyle.None, // inheritance, realization, link
302+
// Composition and aggregation use a diamond marker shape in UML, but the
303+
// current renderer has no distinct diamond marker. Use None here and rely on
304+
// class:relationshipType metadata for future renderer support.
305+
_ => ArrowHeadStyle.None,
297306
};
298307

299308
// ── Operator lookup ───────────────────────────────────────────────────────

tests/DiagramForge.Tests/Parsers/Mermaid/MermaidClassDiagramParserTests.cs

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -361,11 +361,12 @@ class Animal
361361
[Fact]
362362
public void Parse_Inheritance_ForwardOperator_CreatesEdge()
363363
{
364+
// "Animal <|-- Dog": Dog is the child/source, Animal is the parent/target.
364365
var diagram = _parser.Parse("classDiagram\n Animal <|-- Dog");
365366

366367
var edge = Assert.Single(diagram.Edges);
367-
Assert.Equal("Animal", edge.SourceId);
368-
Assert.Equal("Dog", edge.TargetId);
368+
Assert.Equal("Dog", edge.SourceId);
369+
Assert.Equal("Animal", edge.TargetId);
369370
Assert.Equal("inheritance", edge.Metadata["class:relationshipType"]);
370371
Assert.Equal(EdgeLineStyle.Solid, edge.LineStyle);
371372
Assert.Equal(ArrowHeadStyle.None, edge.ArrowHead);
@@ -374,6 +375,7 @@ public void Parse_Inheritance_ForwardOperator_CreatesEdge()
374375
[Fact]
375376
public void Parse_Inheritance_ReverseOperator_CreatesEdge()
376377
{
378+
// "Dog --|> Animal": same relationship as <|--, Dog is still the child/source.
377379
var diagram = _parser.Parse("classDiagram\n Dog --|> Animal");
378380

379381
var edge = Assert.Single(diagram.Edges);
@@ -392,15 +394,18 @@ public void Parse_Composition_ForwardOperator_CreatesEdge()
392394
Assert.Equal("Engine", edge.TargetId);
393395
Assert.Equal("composition", edge.Metadata["class:relationshipType"]);
394396
Assert.Equal(EdgeLineStyle.Solid, edge.LineStyle);
395-
Assert.Equal(ArrowHeadStyle.Diamond, edge.ArrowHead);
397+
Assert.Equal(ArrowHeadStyle.None, edge.ArrowHead);
396398
}
397399

398400
[Fact]
399401
public void Parse_Composition_ReverseOperator_CreatesEdge()
400402
{
403+
// "Engine --* Car": Car is still the whole/source, Engine is the part/target.
401404
var diagram = _parser.Parse("classDiagram\n Engine --* Car");
402405

403406
var edge = Assert.Single(diagram.Edges);
407+
Assert.Equal("Car", edge.SourceId);
408+
Assert.Equal("Engine", edge.TargetId);
404409
Assert.Equal("composition", edge.Metadata["class:relationshipType"]);
405410
}
406411

@@ -413,15 +418,18 @@ public void Parse_Aggregation_ForwardOperator_CreatesEdge()
413418
Assert.Equal("Zoo", edge.SourceId);
414419
Assert.Equal("Animal", edge.TargetId);
415420
Assert.Equal("aggregation", edge.Metadata["class:relationshipType"]);
416-
Assert.Equal(ArrowHeadStyle.Diamond, edge.ArrowHead);
421+
Assert.Equal(ArrowHeadStyle.None, edge.ArrowHead);
417422
}
418423

419424
[Fact]
420425
public void Parse_Aggregation_ReverseOperator_CreatesEdge()
421426
{
427+
// "Animal --o Zoo": Zoo is still the whole/source, Animal is the aggregated/target.
422428
var diagram = _parser.Parse("classDiagram\n Animal --o Zoo");
423429

424430
var edge = Assert.Single(diagram.Edges);
431+
Assert.Equal("Zoo", edge.SourceId);
432+
Assert.Equal("Animal", edge.TargetId);
425433
Assert.Equal("aggregation", edge.Metadata["class:relationshipType"]);
426434
}
427435

@@ -441,9 +449,12 @@ public void Parse_Association_ForwardOperator_CreatesEdge()
441449
[Fact]
442450
public void Parse_Association_ReverseOperator_CreatesEdge()
443451
{
452+
// "Food <-- Animal": same as "Animal --> Food"; Animal is still the source.
444453
var diagram = _parser.Parse("classDiagram\n Food <-- Animal");
445454

446455
var edge = Assert.Single(diagram.Edges);
456+
Assert.Equal("Animal", edge.SourceId);
457+
Assert.Equal("Food", edge.TargetId);
447458
Assert.Equal("association", edge.Metadata["class:relationshipType"]);
448459
}
449460

@@ -476,21 +487,25 @@ public void Parse_Dependency_ForwardOperator_CreatesEdge()
476487
[Fact]
477488
public void Parse_Dependency_ReverseOperator_CreatesEdge()
478489
{
490+
// "Service <.. Client": same as "Client ..> Service"; Client is still the dependent/source.
479491
var diagram = _parser.Parse("classDiagram\n Service <.. Client");
480492

481493
var edge = Assert.Single(diagram.Edges);
494+
Assert.Equal("Client", edge.SourceId);
495+
Assert.Equal("Service", edge.TargetId);
482496
Assert.Equal("dependency", edge.Metadata["class:relationshipType"]);
483497
Assert.Equal(EdgeLineStyle.Dashed, edge.LineStyle);
484498
}
485499

486500
[Fact]
487501
public void Parse_Realization_ForwardOperator_CreatesEdge()
488502
{
503+
// "IFlyable <|.. Bird": Bird is the implementer/source, IFlyable is the interface/target.
489504
var diagram = _parser.Parse("classDiagram\n IFlyable <|.. Bird");
490505

491506
var edge = Assert.Single(diagram.Edges);
492-
Assert.Equal("IFlyable", edge.SourceId);
493-
Assert.Equal("Bird", edge.TargetId);
507+
Assert.Equal("Bird", edge.SourceId);
508+
Assert.Equal("IFlyable", edge.TargetId);
494509
Assert.Equal("realization", edge.Metadata["class:relationshipType"]);
495510
Assert.Equal(EdgeLineStyle.Dashed, edge.LineStyle);
496511
Assert.Equal(ArrowHeadStyle.None, edge.ArrowHead);
@@ -499,9 +514,12 @@ public void Parse_Realization_ForwardOperator_CreatesEdge()
499514
[Fact]
500515
public void Parse_Realization_ReverseOperator_CreatesEdge()
501516
{
517+
// "Bird ..|> IFlyable": same relationship; Bird is still the implementer/source.
502518
var diagram = _parser.Parse("classDiagram\n Bird ..|> IFlyable");
503519

504520
var edge = Assert.Single(diagram.Edges);
521+
Assert.Equal("Bird", edge.SourceId);
522+
Assert.Equal("IFlyable", edge.TargetId);
505523
Assert.Equal("realization", edge.Metadata["class:relationshipType"]);
506524
}
507525

@@ -612,20 +630,22 @@ Car o-- Wheel
612630
// ── Operator reversal metadata ────────────────────────────────────────────
613631

614632
[Fact]
615-
public void Parse_ForwardInheritanceOp_IsReversedIsFalse()
633+
public void Parse_LeftArrowInheritanceOp_OperatorReversedIsTrue()
616634
{
635+
// <|-- has the arrow on the left; isReversed=true causes SourceId/TargetId swap.
617636
var diagram = _parser.Parse("classDiagram\n Animal <|-- Dog");
618637

619638
var edge = Assert.Single(diagram.Edges);
620-
Assert.Equal(false, edge.Metadata["class:operatorReversed"]);
639+
Assert.Equal(true, edge.Metadata["class:operatorReversed"]);
621640
}
622641

623642
[Fact]
624-
public void Parse_ReverseInheritanceOp_IsReversedIsTrue()
643+
public void Parse_RightArrowInheritanceOp_OperatorReversedIsFalse()
625644
{
645+
// --|> has the arrow on the right; isReversed=false, no swap needed.
626646
var diagram = _parser.Parse("classDiagram\n Dog --|> Animal");
627647

628648
var edge = Assert.Single(diagram.Edges);
629-
Assert.Equal(true, edge.Metadata["class:operatorReversed"]);
649+
Assert.Equal(false, edge.Metadata["class:operatorReversed"]);
630650
}
631651
}

0 commit comments

Comments
 (0)