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/shared_base_state_SharedBaseStateLogic.verified.puml b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/shared_base_state_SharedBaseStateLogic.verified.puml new file mode 100644 index 0000000..cb27f5d --- /dev/null +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/snapshots/shared_base_state_SharedBaseStateLogic.verified.puml @@ -0,0 +1,7 @@ +@startuml SharedBaseStateLogic +state SharedBaseStateLogic { + state Dark +} + +[*] --> 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/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/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", "")}"; 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/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..9a02956 --- /dev/null +++ b/Chickensoft.LogicBlocks.DiagramGenerator.Tests/test_cases/shared_base_state/SharedBaseStateLogic.cs @@ -0,0 +1,22 @@ +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; + } +} 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; +} 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 1756cbc..5edf91f 100644 --- a/Chickensoft.LogicBlocks.DiagramGenerator/src/DiagramGenerator.cs +++ b/Chickensoft.LogicBlocks.DiagramGenerator/src/DiagramGenerator.cs @@ -128,24 +128,54 @@ 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) { 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 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); + public T? GetGraph( Func discoverFunc, TypeDeclarationSyntax logicBlockClassDecl, @@ -187,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); @@ -231,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) @@ -375,7 +411,7 @@ CancellationToken token var semanticSymbol = model.GetDeclaredSymbol(stateClassDecl, token); - if (semanticSymbol is null) + if (semanticSymbol is null || !InheritsFromLogicBlockState(semanticSymbol)) { return null; } 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; - } }