From 4c8e5e3aeefc4956d639b2ab6cbce6d61e885835 Mon Sep 17 00:00:00 2001 From: Aleksandr Kulik Date: Mon, 31 Aug 2026 22:17:58 +0200 Subject: [PATCH 1/5] fix: check StateDiagram inheritance semantically, not syntactically --- ...e_state_SharedBaseStateLogic.verified.puml | 11 ++++++++ .../shared_base_state/SharedBaseState.cs | 3 +++ .../SharedBaseStateLogic.States.cs | 22 ++++++++++++++++ .../shared_base_state/SharedBaseStateLogic.cs | 25 +++++++++++++++++++ .../src/DiagramGenerator.cs | 9 ++++++- 5 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/shared_base_state_SharedBaseStateLogic.verified.puml create mode 100644 Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_state/SharedBaseState.cs create mode 100644 Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_state/SharedBaseStateLogic.States.cs create mode 100644 Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_state/SharedBaseStateLogic.cs diff --git a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/shared_base_state_SharedBaseStateLogic.verified.puml b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/shared_base_state_SharedBaseStateLogic.verified.puml new file mode 100644 index 0000000..ae3935f --- /dev/null +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/shared_base_state_SharedBaseStateLogic.verified.puml @@ -0,0 +1,11 @@ +@startuml SharedState +state "SharedState" as Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseStateLogic_SharedState { + state "Dark" as Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseStateLogic_SharedState_Dark + state "Lit" as Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseStateLogic_SharedState_Lit +} + +Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseStateLogic_SharedState_Dark --> Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseStateLogic_SharedState_Lit : Toggle +Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseStateLogic_SharedState_Lit --> Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseStateLogic_SharedState_Dark : Toggle + +Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseStateLogic_SharedState_Lit : OnEnter → Toggled +@enduml \ No newline at end of file diff --git a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_state/SharedBaseState.cs b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_state/SharedBaseState.cs new file mode 100644 index 0000000..4119da7 --- /dev/null +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_state/SharedBaseState.cs @@ -0,0 +1,3 @@ +namespace Chickensoft.LogicBlocks.DiagramGenerator.Tests.TestCases; + +public abstract record SharedBaseState : LogicBlockState; diff --git a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_state/SharedBaseStateLogic.States.cs b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_state/SharedBaseStateLogic.States.cs new file mode 100644 index 0000000..3b69929 --- /dev/null +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_state/SharedBaseStateLogic.States.cs @@ -0,0 +1,22 @@ +namespace Chickensoft.LogicBlocks.DiagramGenerator.Tests.TestCases; + +public partial class SharedBaseStateLogic +{ + public partial record SharedState + { + public partial record Lit : SharedState, IGet + { + public Lit() + { + this.OnEnter(() => Output(new Output.Toggled())); + } + + public Type On(in Input.Toggle input) => To(); + } + + public partial record Dark : SharedState, IGet + { + public Type On(in Input.Toggle input) => To(); + } + } +} diff --git a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_state/SharedBaseStateLogic.cs b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_state/SharedBaseStateLogic.cs new file mode 100644 index 0000000..db7446f --- /dev/null +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_state/SharedBaseStateLogic.cs @@ -0,0 +1,25 @@ +namespace Chickensoft.LogicBlocks.DiagramGenerator.Tests.TestCases; + +public partial class SharedBaseStateLogic : LogicBlock +{ + public SharedBaseStateLogic() + { + Set(new SharedState.Lit()); + Set(new SharedState.Dark()); + } + + public void StartLogicBlock() => Start(); + + public static class Input + { + public readonly record struct Toggle; + } + + public static class Output + { + public readonly record struct Toggled; + } + + [StateDiagram] + public abstract partial record SharedState : SharedBaseState; +} diff --git a/Chickensoft.LogicBlocks.DiagramGenerator/src/DiagramGenerator.cs b/Chickensoft.LogicBlocks.DiagramGenerator/src/DiagramGenerator.cs index 1756cbc..59f5512 100644 --- a/Chickensoft.LogicBlocks.DiagramGenerator/src/DiagramGenerator.cs +++ b/Chickensoft.LogicBlocks.DiagramGenerator/src/DiagramGenerator.cs @@ -141,11 +141,13 @@ public static bool IsLogicBlockCandidate(SyntaxNode node) public static bool IsStateDiagramCandidate(SyntaxNode node) { return node is TypeDeclarationSyntax classDeclarationSyntax && - CodeService.InheritsFromByName(classDeclarationSyntax, Constants.LOGIC_BLOCK_STATE) && classDeclarationSyntax.AttributeLists.SelectMany(l => l.Attributes) .Any(attr => attr.Name.ToString() == Constants.LOGIC_BLOCK_ATTRIBUTE_NAME); } + private static bool InheritsFromLogicBlockState(INamedTypeSymbol symbol) => + CodeService.GetAllBaseTypes(symbol).Any(baseType => baseType.Name == Constants.LOGIC_BLOCK_STATE); + public T? GetGraph( Func discoverFunc, TypeDeclarationSyntax logicBlockClassDecl, @@ -380,6 +382,11 @@ CancellationToken token return null; } + if (!InheritsFromLogicBlockState(semanticSymbol)) + { + return null; + } + var concreteState = semanticSymbol; // Search for all types that inherit from this state From ce39a8227fb4432acba959163dfce1b97f330252 Mon Sep 17 00:00:00 2001 From: Aleksandr Kulik Date: Mon, 5 Oct 2026 23:50:09 +0200 Subject: [PATCH 2/5] Merge null and inheritance checks in DiscoverStateGraph --- .../src/DiagramGenerator.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Chickensoft.LogicBlocks.DiagramGenerator/src/DiagramGenerator.cs b/Chickensoft.LogicBlocks.DiagramGenerator/src/DiagramGenerator.cs index 59f5512..c1f0521 100644 --- a/Chickensoft.LogicBlocks.DiagramGenerator/src/DiagramGenerator.cs +++ b/Chickensoft.LogicBlocks.DiagramGenerator/src/DiagramGenerator.cs @@ -377,12 +377,7 @@ CancellationToken token var semanticSymbol = model.GetDeclaredSymbol(stateClassDecl, token); - if (semanticSymbol is null) - { - return null; - } - - if (!InheritsFromLogicBlockState(semanticSymbol)) + if (semanticSymbol is null || !InheritsFromLogicBlockState(semanticSymbol)) { return null; } From c0266b622596a531485a6684ad9fc324a2dd7333 Mon Sep 17 00:00:00 2001 From: Aleksandr Kulik Date: Mon, 5 Oct 2026 23:53:37 +0200 Subject: [PATCH 3/5] test: fail when an expected diagram is not generated --- .../test/DiagrammerTest.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test/DiagrammerTest.cs b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test/DiagrammerTest.cs index 5388e11..6e14072 100644 --- a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test/DiagrammerTest.cs +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test/DiagrammerTest.cs @@ -81,6 +81,19 @@ await File.ReadAllTextAsync(filePath), Assert.Fail("No generated files were found."); } + var testDirName = Path.GetFileName(testFolderPath); + var expectedDiagrams = Directory + .GetFiles(CurrentDir("../snapshots"), $"{testDirName}_*.verified.puml") + .Select(path => Path.GetFileName(path).Replace(".verified.puml", "")); + var generatedDiagrams = generatedPumls + .Select(path => $"{testDirName}_{Path.GetFileName(path).Replace(".g.puml", "")}"); + var missingDiagrams = expectedDiagrams.Except(generatedDiagrams).ToList(); + + if (missingDiagrams.Count != 0) + { + Assert.Fail($"Expected diagrams were not generated: {string.Join(", ", missingDiagrams)}"); + } + foreach (var pumlPath in generatedPumls) { var testName = $"{Path.GetFileName(testFolderPath)}_{Path.GetFileName(pumlPath).Replace(".g.puml", "")}"; From d196788ea6293537ff07064a05767aca016c0555 Mon Sep 17 00:00:00 2001 From: Aleksandr Kulik Date: Mon, 5 Oct 2026 23:56:46 +0200 Subject: [PATCH 4/5] move SharedState into its own file --- ...red_base_state_SharedBaseStateLogic.verified.puml | 12 ++++-------- .../shared_base_state_SharedState.verified.puml | 11 +++++++++++ .../shared_base_state/SharedBaseStateLogic.cs | 3 --- .../test_cases/shared_base_state/SharedState.cs | 7 +++++++ 4 files changed, 22 insertions(+), 11 deletions(-) create mode 100644 Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/shared_base_state_SharedState.verified.puml create mode 100644 Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_state/SharedState.cs diff --git a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/shared_base_state_SharedBaseStateLogic.verified.puml b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/shared_base_state_SharedBaseStateLogic.verified.puml index ae3935f..cb27f5d 100644 --- a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/shared_base_state_SharedBaseStateLogic.verified.puml +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/shared_base_state_SharedBaseStateLogic.verified.puml @@ -1,11 +1,7 @@ -@startuml SharedState -state "SharedState" as Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseStateLogic_SharedState { - state "Dark" as Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseStateLogic_SharedState_Dark - state "Lit" as Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseStateLogic_SharedState_Lit +@startuml SharedBaseStateLogic +state SharedBaseStateLogic { + state Dark } -Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseStateLogic_SharedState_Dark --> Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseStateLogic_SharedState_Lit : Toggle -Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseStateLogic_SharedState_Lit --> Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseStateLogic_SharedState_Dark : Toggle - -Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseStateLogic_SharedState_Lit : OnEnter → Toggled +[*] --> Dark @enduml \ No newline at end of file diff --git a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/shared_base_state_SharedState.verified.puml b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/shared_base_state_SharedState.verified.puml new file mode 100644 index 0000000..ae3935f --- /dev/null +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/shared_base_state_SharedState.verified.puml @@ -0,0 +1,11 @@ +@startuml SharedState +state "SharedState" as Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseStateLogic_SharedState { + state "Dark" as Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseStateLogic_SharedState_Dark + state "Lit" as Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseStateLogic_SharedState_Lit +} + +Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseStateLogic_SharedState_Dark --> Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseStateLogic_SharedState_Lit : Toggle +Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseStateLogic_SharedState_Lit --> Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseStateLogic_SharedState_Dark : Toggle + +Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseStateLogic_SharedState_Lit : OnEnter → Toggled +@enduml \ No newline at end of file diff --git a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_state/SharedBaseStateLogic.cs b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_state/SharedBaseStateLogic.cs index db7446f..9a02956 100644 --- a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_state/SharedBaseStateLogic.cs +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_state/SharedBaseStateLogic.cs @@ -19,7 +19,4 @@ public static class Output { public readonly record struct Toggled; } - - [StateDiagram] - public abstract partial record SharedState : SharedBaseState; } diff --git a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_state/SharedState.cs b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_state/SharedState.cs new file mode 100644 index 0000000..7ab2d49 --- /dev/null +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_state/SharedState.cs @@ -0,0 +1,7 @@ +namespace Chickensoft.LogicBlocks.DiagramGenerator.Tests.TestCases; + +public partial class SharedBaseStateLogic +{ + [StateDiagram] + public abstract partial record SharedState : SharedBaseState; +} From dedc8463ee4ba211a70ac18e600abe1c66359fb8 Mon Sep 17 00:00:00 2001 From: Aleksandr Kulik Date: Tue, 6 Oct 2026 00:44:50 +0200 Subject: [PATCH 5/5] detect LogicBlock inheritance semantically, emit one diagram per partial LogicBlock --- .../nested_start_call_BaseState.verified.puml | 11 ++++ ...d_base_logic_block_BaseState.verified.puml | 11 ++++ ...c_block_SharedBaseLogicBlock.verified.puml | 7 +++ ..._multiple_partials_BaseState.verified.puml | 11 ++++ ...ple_partials_MultiStartLogic.verified.puml | 9 +++ .../test_cases/nested_start_call/BaseState.cs | 23 +++++++ .../NestedStartLogic.Helpers.cs | 11 ++++ .../nested_start_call/NestedStartLogic.cs | 20 ++++++ .../shared_base_logic_block/BaseState.cs | 23 +++++++ .../SharedBaseLogic.cs | 3 + .../SharedBaseLogicBlock.cs | 22 +++++++ .../start_in_multiple_partials/BaseState.cs | 23 +++++++ .../MultiStartLogic.Restart.cs | 6 ++ .../MultiStartLogic.cs | 22 +++++++ .../src/DiagramGenerator.cs | 62 ++++++++++++++----- .../src/services/CodeService.cs | 50 --------------- 16 files changed, 250 insertions(+), 64 deletions(-) create mode 100644 Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/nested_start_call_BaseState.verified.puml create mode 100644 Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/shared_base_logic_block_BaseState.verified.puml create mode 100644 Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/shared_base_logic_block_SharedBaseLogicBlock.verified.puml create mode 100644 Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/start_in_multiple_partials_BaseState.verified.puml create mode 100644 Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/start_in_multiple_partials_MultiStartLogic.verified.puml create mode 100644 Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/nested_start_call/BaseState.cs create mode 100644 Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/nested_start_call/NestedStartLogic.Helpers.cs create mode 100644 Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/nested_start_call/NestedStartLogic.cs create mode 100644 Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_logic_block/BaseState.cs create mode 100644 Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_logic_block/SharedBaseLogic.cs create mode 100644 Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_logic_block/SharedBaseLogicBlock.cs create mode 100644 Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/start_in_multiple_partials/BaseState.cs create mode 100644 Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/start_in_multiple_partials/MultiStartLogic.Restart.cs create mode 100644 Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/start_in_multiple_partials/MultiStartLogic.cs diff --git a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/nested_start_call_BaseState.verified.puml b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/nested_start_call_BaseState.verified.puml new file mode 100644 index 0000000..3db6cf0 --- /dev/null +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/nested_start_call_BaseState.verified.puml @@ -0,0 +1,11 @@ +@startuml BaseState +state "BaseState" as Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_NestedStartLogic_BaseState { + state "Idle" as Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_NestedStartLogic_BaseState_Idle + state "Running" as Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_NestedStartLogic_BaseState_Running +} + +Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_NestedStartLogic_BaseState_Idle --> Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_NestedStartLogic_BaseState_Running : Toggle +Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_NestedStartLogic_BaseState_Running --> Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_NestedStartLogic_BaseState_Idle : Toggle + +Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_NestedStartLogic_BaseState_Running : OnEnter → Toggled +@enduml \ No newline at end of file diff --git a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/shared_base_logic_block_BaseState.verified.puml b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/shared_base_logic_block_BaseState.verified.puml new file mode 100644 index 0000000..3d7227d --- /dev/null +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/shared_base_logic_block_BaseState.verified.puml @@ -0,0 +1,11 @@ +@startuml BaseState +state "BaseState" as Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseLogicBlock_BaseState { + state "Idle" as Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseLogicBlock_BaseState_Idle + state "Running" as Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseLogicBlock_BaseState_Running +} + +Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseLogicBlock_BaseState_Idle --> Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseLogicBlock_BaseState_Running : Toggle +Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseLogicBlock_BaseState_Running --> Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseLogicBlock_BaseState_Idle : Toggle + +Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_SharedBaseLogicBlock_BaseState_Running : OnEnter → Toggled +@enduml \ No newline at end of file diff --git a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/shared_base_logic_block_SharedBaseLogicBlock.verified.puml b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/shared_base_logic_block_SharedBaseLogicBlock.verified.puml new file mode 100644 index 0000000..8ad61b0 --- /dev/null +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/shared_base_logic_block_SharedBaseLogicBlock.verified.puml @@ -0,0 +1,7 @@ +@startuml SharedBaseLogicBlock +state SharedBaseLogicBlock { + state Idle +} + +[*] --> Idle +@enduml \ No newline at end of file diff --git a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/start_in_multiple_partials_BaseState.verified.puml b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/start_in_multiple_partials_BaseState.verified.puml new file mode 100644 index 0000000..70b8ad8 --- /dev/null +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/start_in_multiple_partials_BaseState.verified.puml @@ -0,0 +1,11 @@ +@startuml BaseState +state "BaseState" as Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_MultiStartLogic_BaseState { + state "Idle" as Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_MultiStartLogic_BaseState_Idle + state "Running" as Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_MultiStartLogic_BaseState_Running +} + +Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_MultiStartLogic_BaseState_Idle --> Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_MultiStartLogic_BaseState_Running : Toggle +Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_MultiStartLogic_BaseState_Running --> Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_MultiStartLogic_BaseState_Idle : Toggle + +Chickensoft_LogicBlocks_DiagramGenerator_Tests_TestCases_MultiStartLogic_BaseState_Running : OnEnter → Toggled +@enduml \ No newline at end of file diff --git a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/start_in_multiple_partials_MultiStartLogic.verified.puml b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/start_in_multiple_partials_MultiStartLogic.verified.puml new file mode 100644 index 0000000..7322054 --- /dev/null +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/start_in_multiple_partials_MultiStartLogic.verified.puml @@ -0,0 +1,9 @@ +@startuml MultiStartLogic +state MultiStartLogic { + state Idle + state Running +} + +[*] --> Idle +[*] --> Running +@enduml \ No newline at end of file diff --git a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/nested_start_call/BaseState.cs b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/nested_start_call/BaseState.cs new file mode 100644 index 0000000..3253478 --- /dev/null +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/nested_start_call/BaseState.cs @@ -0,0 +1,23 @@ +namespace Chickensoft.LogicBlocks.DiagramGenerator.Tests.TestCases; + +public partial class NestedStartLogic +{ + [StateDiagram] + public abstract partial record BaseState : LogicBlockState + { + public partial record Running : BaseState, IGet + { + public Running() + { + this.OnEnter(() => Output(new Output.Toggled())); + } + + public Type On(in Input.Toggle input) => To(); + } + + public partial record Idle : BaseState, IGet + { + public Type On(in Input.Toggle input) => To(); + } + } +} diff --git a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/nested_start_call/NestedStartLogic.Helpers.cs b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/nested_start_call/NestedStartLogic.Helpers.cs new file mode 100644 index 0000000..4c6c045 --- /dev/null +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/nested_start_call/NestedStartLogic.Helpers.cs @@ -0,0 +1,11 @@ +namespace Chickensoft.LogicBlocks.DiagramGenerator.Tests.TestCases; + +public partial class NestedStartLogic +{ + public class Helper + { + public void Start() { } + + public void Run() => Start(); + } +} diff --git a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/nested_start_call/NestedStartLogic.cs b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/nested_start_call/NestedStartLogic.cs new file mode 100644 index 0000000..6e9dc4e --- /dev/null +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/nested_start_call/NestedStartLogic.cs @@ -0,0 +1,20 @@ +namespace Chickensoft.LogicBlocks.DiagramGenerator.Tests.TestCases; + +public partial class NestedStartLogic : LogicBlock +{ + public NestedStartLogic() + { + Set(new BaseState.Running()); + Set(new BaseState.Idle()); + } + + public static class Input + { + public readonly record struct Toggle; + } + + public static class Output + { + public readonly record struct Toggled; + } +} diff --git a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_logic_block/BaseState.cs b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_logic_block/BaseState.cs new file mode 100644 index 0000000..630cbdd --- /dev/null +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_logic_block/BaseState.cs @@ -0,0 +1,23 @@ +namespace Chickensoft.LogicBlocks.DiagramGenerator.Tests.TestCases; + +public partial class SharedBaseLogicBlock +{ + [StateDiagram] + public abstract partial record BaseState : LogicBlockState + { + public partial record Running : BaseState, IGet + { + public Running() + { + this.OnEnter(() => Output(new Output.Toggled())); + } + + public Type On(in Input.Toggle input) => To(); + } + + public partial record Idle : BaseState, IGet + { + public Type On(in Input.Toggle input) => To(); + } + } +} diff --git a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_logic_block/SharedBaseLogic.cs b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_logic_block/SharedBaseLogic.cs new file mode 100644 index 0000000..837890b --- /dev/null +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_logic_block/SharedBaseLogic.cs @@ -0,0 +1,3 @@ +namespace Chickensoft.LogicBlocks.DiagramGenerator.Tests.TestCases; + +public abstract class SharedBaseLogic : LogicBlock; diff --git a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_logic_block/SharedBaseLogicBlock.cs b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_logic_block/SharedBaseLogicBlock.cs new file mode 100644 index 0000000..4934786 --- /dev/null +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_logic_block/SharedBaseLogicBlock.cs @@ -0,0 +1,22 @@ +namespace Chickensoft.LogicBlocks.DiagramGenerator.Tests.TestCases; + +public partial class SharedBaseLogicBlock : SharedBaseLogic +{ + public SharedBaseLogicBlock() + { + Set(new BaseState.Running()); + Set(new BaseState.Idle()); + } + + public void StartLogicBlock() => Start(); + + public static class Input + { + public readonly record struct Toggle; + } + + public static class Output + { + public readonly record struct Toggled; + } +} diff --git a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/start_in_multiple_partials/BaseState.cs b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/start_in_multiple_partials/BaseState.cs new file mode 100644 index 0000000..aca1a9f --- /dev/null +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/start_in_multiple_partials/BaseState.cs @@ -0,0 +1,23 @@ +namespace Chickensoft.LogicBlocks.DiagramGenerator.Tests.TestCases; + +public partial class MultiStartLogic +{ + [StateDiagram] + public abstract partial record BaseState : LogicBlockState + { + public partial record Running : BaseState, IGet + { + public Running() + { + this.OnEnter(() => Output(new Output.Toggled())); + } + + public Type On(in Input.Toggle input) => To(); + } + + public partial record Idle : BaseState, IGet + { + public Type On(in Input.Toggle input) => To(); + } + } +} diff --git a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/start_in_multiple_partials/MultiStartLogic.Restart.cs b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/start_in_multiple_partials/MultiStartLogic.Restart.cs new file mode 100644 index 0000000..131c1df --- /dev/null +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/start_in_multiple_partials/MultiStartLogic.Restart.cs @@ -0,0 +1,6 @@ +namespace Chickensoft.LogicBlocks.DiagramGenerator.Tests.TestCases; + +public partial class MultiStartLogic +{ + public void RestartLogicBlock() => Start(); +} diff --git a/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/start_in_multiple_partials/MultiStartLogic.cs b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/start_in_multiple_partials/MultiStartLogic.cs new file mode 100644 index 0000000..0d094df --- /dev/null +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/start_in_multiple_partials/MultiStartLogic.cs @@ -0,0 +1,22 @@ +namespace Chickensoft.LogicBlocks.DiagramGenerator.Tests.TestCases; + +public partial class MultiStartLogic : LogicBlock +{ + public MultiStartLogic() + { + Set(new BaseState.Running()); + Set(new BaseState.Idle()); + } + + public void StartLogicBlock() => Start(); + + public static class Input + { + public readonly record struct Toggle; + } + + public static class Output + { + public readonly record struct Toggled; + } +} diff --git a/Chickensoft.LogicBlocks.DiagramGenerator/src/DiagramGenerator.cs b/Chickensoft.LogicBlocks.DiagramGenerator/src/DiagramGenerator.cs index c1f0521..5edf91f 100644 --- a/Chickensoft.LogicBlocks.DiagramGenerator/src/DiagramGenerator.cs +++ b/Chickensoft.LogicBlocks.DiagramGenerator/src/DiagramGenerator.cs @@ -128,14 +128,38 @@ public void GenerateDiagram(SourceProductionContext context, GenerationData data public static bool IsLogicBlockCandidate(SyntaxNode node) { - //Only retrieve logic blocks that have the LogicBlock type and have the Start method called - return node is ClassDeclarationSyntax classDeclaration && - (CodeService.InheritsFromByName(classDeclaration, Constants.LOGIC_BLOCK_TYPE_NAME) || - CodeService.InheritsFromByName(classDeclaration, Constants.AUTO_BLOCK_TYPE_NAME)) && - classDeclaration.DescendantNodes().OfType() - .Any(invocation => - (invocation.Expression as SimpleNameSyntax)?.Identifier.Text == Constants.LOGIC_BLOCK_STATE_LOGIC_START || - (invocation.Expression as GenericNameSyntax)?.Identifier.Text == Constants.LOGIC_BLOCK_STATE_LOGIC_START); + // Only retrieve classes that call Start; inheritance is checked in DiscoverLogicDiagram + return node is ClassDeclarationSyntax classDeclaration && CallsStart(classDeclaration); + } + + // Nested types are skipped: their Start calls aren't the LogicBlock's. + private static bool CallsStart(SyntaxNode declaration) => + declaration + .DescendantNodes(node => node == declaration || node is not TypeDeclarationSyntax) + .OfType() + .Any(invocation => + invocation.Expression is SimpleNameSyntax { Identifier.Text: Constants.LOGIC_BLOCK_STATE_LOGIC_START }); + + /// + /// Determines whether this declaration owns the diagram when Start is called + /// from several partial declarations of the same LogicBlock + /// + private static bool OwnsLogicDiagram( + TypeDeclarationSyntax declaration, + INamedTypeSymbol symbol, + CancellationToken token + ) + { + var owner = symbol.DeclaringSyntaxReferences + .Select(reference => reference.GetSyntax(token)) + .OfType() + .Where(CallsStart) + .OrderByDescending(syntax => syntax.BaseList is not null) + .ThenBy(syntax => syntax.SyntaxTree.FilePath, StringComparer.Ordinal) + .First(); + + return owner.SyntaxTree == declaration.SyntaxTree && + owner.SpanStart == declaration.SpanStart; } public static bool IsStateDiagramCandidate(SyntaxNode node) @@ -145,6 +169,10 @@ public static bool IsStateDiagramCandidate(SyntaxNode node) .Any(attr => attr.Name.ToString() == Constants.LOGIC_BLOCK_ATTRIBUTE_NAME); } + private static bool InheritsFromLogicBlock(INamedTypeSymbol symbol) => + CodeService.GetAllBaseTypes(symbol).Any(baseType => + baseType.Name is Constants.LOGIC_BLOCK_TYPE_NAME or Constants.AUTO_BLOCK_TYPE_NAME); + private static bool InheritsFromLogicBlockState(INamedTypeSymbol symbol) => CodeService.GetAllBaseTypes(symbol).Any(baseType => baseType.Name == Constants.LOGIC_BLOCK_STATE); @@ -189,29 +217,35 @@ CancellationToken token var semanticSymbol = model.GetDeclaredSymbol(logicBlockClassDecl, token); - if (semanticSymbol is null) + if (semanticSymbol is null || + !InheritsFromLogicBlock(semanticSymbol) || + !OwnsLogicDiagram(logicBlockClassDecl, semanticSymbol, token)) { return null; } HashSet initialStateIds = []; + // Start calls may live in other partial files + SemanticModel modelFor(SyntaxNode node) => + node.SyntaxTree == model.SyntaxTree ? model : model.Compilation.GetSemanticModel(node.SyntaxTree); + var startMethodArgs = semanticSymbol.DeclaringSyntaxReferences .SelectMany(x => x.GetSyntax() .DescendantNodes() .OfType() .Where(invocation => - (invocation.Expression as SimpleNameSyntax)?.Identifier.Text == Constants.LOGIC_BLOCK_STATE_LOGIC_START && - invocation.ArgumentList.Arguments is { Count: not 0 } args&& - model.GetTypeInfo(args[0].Expression).Type?.ToDisplayString() == Constants.SYSTEMTYPE) + invocation.Expression is SimpleNameSyntax { Identifier.Text: Constants.LOGIC_BLOCK_STATE_LOGIC_START } && + invocation.ArgumentList.Arguments is { Count: not 0 } args && + modelFor(invocation).GetTypeInfo(args[0].Expression).Type?.ToDisplayString() == Constants.SYSTEMTYPE) .Select(syntax => syntax.ArgumentList.Arguments[0]) ); foreach (var arg in startMethodArgs) { var initialStateVisitor = new ReturnTypeVisitor( - model, token, CodeService, semanticSymbol + modelFor(arg), token, CodeService, semanticSymbol ); initialStateVisitor.Visit(arg); initialStateIds.UnionWith(initialStateVisitor.ReturnTypes); @@ -233,7 +267,7 @@ invocation.Expression is GenericNameSyntax generic && foreach (var typeArg in genericStartTypeArgs) { - if (model.GetTypeInfo(typeArg, token).Type is INamedTypeSymbol typeSymbol) + if (modelFor(typeArg).GetTypeInfo(typeArg, token).Type is INamedTypeSymbol typeSymbol) { initialStateIds.Add( CodeService.GetNameFullyQualifiedWithoutGenerics(typeSymbol, typeSymbol.Name) diff --git a/Chickensoft.LogicBlocks.DiagramGenerator/src/services/CodeService.cs b/Chickensoft.LogicBlocks.DiagramGenerator/src/services/CodeService.cs index b78835e..6358ed7 100644 --- a/Chickensoft.LogicBlocks.DiagramGenerator/src/services/CodeService.cs +++ b/Chickensoft.LogicBlocks.DiagramGenerator/src/services/CodeService.cs @@ -4,7 +4,6 @@ namespace Chickensoft.LogicBlocks.DiagramGenerator.Services; using System.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; /// /// Common code operations for syntax nodes and semantic model symbols. @@ -62,8 +61,6 @@ IEnumerable GetAllDerivedTypes( Compilation compilation, Func predicate ); - - bool InheritsFromByName(TypeDeclarationSyntax typeDeclarationSyntax, string logicBlockTypeName); } /// @@ -156,51 +153,4 @@ private IEnumerable GetAllTypesInNamespace(INamespaceSymbol ns } } } - - /// - /// Recursively checks whether a type declaration inherits from a type with - /// the given simple name, walking the syntax tree of the same file for - /// intermediate base type declarations. - /// - public bool InheritsFromByName( - TypeDeclarationSyntax typeDecl, - string targetName - ) - { - var root = typeDecl.SyntaxTree.GetRoot(); - return InheritsFromByName(typeDecl, targetName, root, []); - } - - private static bool InheritsFromByName( - TypeDeclarationSyntax typeDecl, - string targetName, - SyntaxNode root, - HashSet visited - ) - { - if (!visited.Add(typeDecl.Identifier.Text)) { return false; } - if (typeDecl.BaseList is null) { return false; } - - foreach (var baseTypeSyntax in typeDecl.BaseList.Types) - { - // Strip generics and namespace qualifiers to get the simple name. - var rawName = baseTypeSyntax.Type.ToString(); - var simpleName = rawName.Split('<')[0].Split('.').Last(); - - if (simpleName == targetName) { return true; } - - // Try to find the base type declaration in the same file and recurse. - var baseDecl = root.DescendantNodes() - .OfType() - .FirstOrDefault(t => t.Identifier.Text == simpleName); - - if (baseDecl is not null && - InheritsFromByName(baseDecl, targetName, root, visited)) - { - return true; - } - } - - return false; - } }