Skip to content

Commit 0f4b292

Browse files
committed
Change the diagram magic attribute to be the output filename.
1 parent 768a99a commit 0f4b292

6 files changed

Lines changed: 86 additions & 64 deletions

File tree

FunctionalStateMachine.Diagrams/StateMachineDiagramGenerator.cs

Lines changed: 79 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -12,91 +12,105 @@ namespace FunctionalStateMachine.Diagrams
1212
[Generator]
1313
public sealed class StateMachineDiagramGenerator : IIncrementalGenerator
1414
{
15-
private const string AttributeName = "FunctionalStateMachine.Diagrams.StateMachineDiagramAttribute";
15+
private const string AttributeName = "FunctionalStateMachine.Diagrams.StateMachineDiagramAttribute";
1616

17-
public void Initialize(IncrementalGeneratorInitializationContext context)
18-
{
19-
context.RegisterPostInitializationOutput(ctx =>
17+
public void Initialize(IncrementalGeneratorInitializationContext context)
2018
{
21-
ctx.AddSource("StateMachineDiagramAttribute.g.cs", """
22-
using System;
19+
context.RegisterPostInitializationOutput(ctx =>
20+
{
21+
ctx.AddSource(
22+
"StateMachineDiagramAttribute.g.cs",
23+
"""
24+
using System;
2325
24-
namespace FunctionalStateMachine.Diagrams
25-
{
26-
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
26+
namespace FunctionalStateMachine.Diagrams
27+
{
28+
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
2729
internal sealed class StateMachineDiagramAttribute : Attribute
2830
{
29-
public StateMachineDiagramAttribute(string name)
31+
public StateMachineDiagramAttribute(string outputPath)
3032
{
31-
Name = name;
33+
OutputPath = outputPath;
3234
}
3335
34-
public string Name { get; }
36+
public string OutputPath { get; }
3537
}
3638
}
3739
""");
3840
});
3941

40-
var diagrams = context.SyntaxProvider.ForAttributeWithMetadataName(
41-
AttributeName,
42-
static (node, _) => node is MethodDeclarationSyntax,
43-
static (ctx, _) => (MethodDeclarationSyntax)ctx.TargetNode)
44-
.Combine(context.CompilationProvider)
45-
.Combine(context.AnalyzerConfigOptionsProvider);
42+
var diagrams = context.SyntaxProvider.ForAttributeWithMetadataName(
43+
AttributeName,
44+
static (node, _) => node is MethodDeclarationSyntax,
45+
static (ctx, _) => (MethodDeclarationSyntax)ctx.TargetNode)
46+
.Combine(context.CompilationProvider)
47+
.Combine(context.AnalyzerConfigOptionsProvider);
4648

47-
context.RegisterSourceOutput(diagrams, static (ctx, data) =>
48-
{
49-
var ((methodSyntax, compilation), options) = data;
50-
if (!options.GlobalOptions.TryGetValue("build_property.ProjectDir", out var projectDir))
51-
{
52-
return;
53-
}
49+
context.RegisterSourceOutput(
50+
diagrams,
51+
static (ctx, data) =>
52+
{
53+
var ((methodSyntax, compilation), options) = data;
54+
if (!options.GlobalOptions.TryGetValue("build_property.ProjectDir", out var projectDir))
55+
{
56+
return;
57+
}
5458

55-
var model = compilation.GetSemanticModel(methodSyntax.SyntaxTree);
56-
if (model.GetDeclaredSymbol(methodSyntax, ctx.CancellationToken) is not IMethodSymbol methodSymbol)
57-
{
58-
return;
59-
}
59+
var model = compilation.GetSemanticModel(methodSyntax.SyntaxTree);
60+
if (model.GetDeclaredSymbol(methodSyntax, ctx.CancellationToken) is not IMethodSymbol methodSymbol)
61+
{
62+
return;
63+
}
64+
65+
var attribute = methodSymbol.GetAttributes()
66+
.FirstOrDefault(attr => attr.AttributeClass?.ToDisplayString() == AttributeName);
67+
if (attribute is null)
68+
{
69+
return;
70+
}
6071

61-
var attribute = methodSymbol.GetAttributes()
62-
.FirstOrDefault(attr => attr.AttributeClass?.ToDisplayString() == AttributeName);
63-
if (attribute is null)
72+
var outputPathValue = attribute.ConstructorArguments.Length == 1
73+
? attribute.ConstructorArguments[0].Value?.ToString()
74+
: null;
75+
if (string.IsNullOrWhiteSpace(outputPathValue))
6476
{
65-
return;
77+
outputPathValue = $"{methodSymbol.Name}.md";
6678
}
6779

68-
var diagramName = attribute.ConstructorArguments.Length == 1
69-
? attribute.ConstructorArguments[0].Value?.ToString()
70-
: methodSymbol.Name;
80+
var diagramName = Path.GetFileNameWithoutExtension(outputPathValue);
7181
if (string.IsNullOrWhiteSpace(diagramName))
7282
{
7383
diagramName = methodSymbol.Name;
7484
}
7585

76-
var chains = DiagramBuilder.GetInvocationChains(methodSyntax);
77-
var diagram = DiagramBuilder.BuildDiagram(diagramName!, chains);
78-
if (diagram is null)
79-
{
80-
return;
81-
}
82-
83-
var outputDir = Path.Combine(projectDir, "diagrams");
84-
Directory.CreateDirectory(outputDir);
85-
var outputPath = Path.Combine(outputDir, $"{diagramName}.md");
86+
var chains = DiagramBuilder.GetInvocationChains(methodSyntax);
87+
var diagram = DiagramBuilder.BuildDiagram(diagramName!, chains);
88+
if (diagram is null)
89+
{
90+
return;
91+
}
8692

87-
if (File.Exists(outputPath))
93+
var outputPath = Path.IsPathRooted(outputPathValue)
94+
? outputPathValue
95+
: Path.Combine(projectDir, outputPathValue);
96+
var outputDir = Path.GetDirectoryName(outputPath);
97+
if (!string.IsNullOrWhiteSpace(outputDir))
8898
{
89-
var existing = File.ReadAllText(outputPath);
90-
if (string.Equals(existing, diagram, StringComparison.Ordinal))
91-
{
92-
return;
93-
}
99+
Directory.CreateDirectory(outputDir);
94100
}
95101

96-
File.WriteAllText(outputPath, diagram);
97-
});
98-
}
102+
if (File.Exists(outputPath))
103+
{
104+
var existing = File.ReadAllText(outputPath);
105+
if (string.Equals(existing, diagram, StringComparison.Ordinal))
106+
{
107+
return;
108+
}
109+
}
99110

111+
File.WriteAllText(outputPath, diagram);
112+
});
113+
}
100114
}
101115
}
102116

@@ -146,17 +160,20 @@ public static List<List<InvocationInfo>> GetInvocationChains(MethodDeclarationSy
146160
{
147161
states.Add(startState);
148162
}
163+
149164
break;
150165
case "For":
151166
if (pendingTrigger && !hasTransition && currentState != null && currentTrigger != null)
152167
{
153168
transitions.Add(new Transition(currentState, currentState, currentTrigger));
154169
}
170+
155171
currentState = GetFirstArg(step);
156172
if (currentState != null)
157173
{
158174
states.Add(currentState);
159175
}
176+
160177
currentTrigger = null;
161178
pendingTrigger = false;
162179
hasTransition = false;
@@ -168,12 +185,14 @@ public static List<List<InvocationInfo>> GetInvocationChains(MethodDeclarationSy
168185
childToParent[currentState] = parent;
169186
states.Add(parent);
170187
}
188+
171189
break;
172190
case "On":
173191
if (pendingTrigger && !hasTransition && currentState != null && currentTrigger != null)
174192
{
175193
transitions.Add(new Transition(currentState, currentState, currentTrigger));
176194
}
195+
177196
currentTrigger = GetTriggerLabel(step);
178197
pendingTrigger = currentTrigger != null;
179198
hasTransition = false;
@@ -188,13 +207,15 @@ public static List<List<InvocationInfo>> GetInvocationChains(MethodDeclarationSy
188207
hasTransition = true;
189208
pendingTrigger = false;
190209
}
210+
191211
break;
192212
case "Build":
193213
if (pendingTrigger && !hasTransition && currentState != null && currentTrigger != null)
194214
{
195215
transitions.Add(new Transition(currentState, currentState, currentTrigger));
196216
pendingTrigger = false;
197217
}
218+
198219
break;
199220
}
200221
}
@@ -376,12 +397,13 @@ private static void RenderState(
376397
{
377398
sb.AppendLine($"{Indent(depth)}{ids[state]}[{state}]");
378399
}
400+
379401
return;
380402
}
381403

382404
sb.AppendLine($"{Indent(depth)}subgraph SG_{Sanitize(state)}[{state}]");
383405
var shouldRenderParentNode = transitionStates.Contains(state)
384-
|| string.Equals(startState, state, StringComparison.Ordinal);
406+
|| string.Equals(startState, state, StringComparison.Ordinal);
385407
if (shouldRenderParentNode && rendered.Add(state))
386408
{
387409
sb.AppendLine($"{Indent(depth + 1)}{ids[state]}[{state}]");

FunctionalStateMachine.Samples/LightSwitchSample.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace FunctionalStateMachine.Samples;
66

77
public static class LightSwitchSample
88
{
9-
[StateMachineDiagram("LightSwitch")]
9+
[StateMachineDiagram("diagrams/LightSwitch.md")]
1010
public static StateMachine<LightState, LightTrigger, LightCommand> Build() =>
1111
StateMachine<LightState, LightTrigger, LightCommand>.Create()
1212
.StartWith(LightState.Off)

FunctionalStateMachine.Samples/SessionLoginSample.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace FunctionalStateMachine.Samples;
55

66
public static class SessionLoginSample
77
{
8-
[StateMachineDiagram("SessionLogin")]
8+
[StateMachineDiagram("diagrams/SessionLogin.md")]
99
public static StateMachine<SessionState, SessionTrigger, SessionData, SessionCommand> Build()
1010
{
1111
return StateMachine<SessionState, SessionTrigger, SessionData, SessionCommand>.Create()

FunctionalStateMachine.Samples/ShoppingTrolleySample.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace FunctionalStateMachine.Samples;
66

77
public static class ShoppingTrolleySample
88
{
9-
[StateMachineDiagram("ShoppingTrolley")]
9+
[StateMachineDiagram("diagrams/ShoppingTrolley.md")]
1010
public static StateMachine<ShopState, CartTrigger, CartSession, ShopCommand> Build()
1111
{
1212
return StateMachine<ShopState, CartTrigger, CartSession, ShopCommand>.Create()

FunctionalStateMachine.Samples/TimerSample.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace FunctionalStateMachine.Samples;
55

66
public static class TimerSample
77
{
8-
[StateMachineDiagram("Timer")]
8+
[StateMachineDiagram("diagrams/Timer.md")]
99
public static StateMachine<TimerState, TimerTrigger, TimerData, TimerCommand> Build()
1010
{
1111
return StateMachine<TimerState, TimerTrigger, TimerData, TimerCommand>.Create()

docs/diagrams.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
Use the diagrams source generator to emit a Mermaid flowchart from your builder method.
44

5-
Simply add the `[StateMachineDiagram]` attribute to your state machine builder method.
6-
A markdown diagram will be emitted into the `diagrams` folder.
5+
Add the `[StateMachineDiagram]` attribute to your builder method and supply the output path.
6+
The generator writes to that location relative to the project directory and creates folders as needed.
77

88
## Why it is useful
99

@@ -16,7 +16,7 @@ A markdown diagram will be emitted into the `diagrams` folder.
1616
```csharp
1717
using FunctionalStateMachine.Diagrams;
1818

19-
[StateMachineDiagram("LightSwitch")]
19+
[StateMachineDiagram("diagrams/LightSwitch.md")]
2020
public static StateMachine<LightState, LightTrigger, LightCommand> Build()
2121
{
2222
return StateMachine<LightState, LightTrigger, LightCommand>.Create()

0 commit comments

Comments
 (0)