Skip to content

Commit 614564d

Browse files
committed
Use Roslyn to parse and extend state classes.
1 parent b74351c commit 614564d

File tree

8 files changed

+159
-99
lines changed

8 files changed

+159
-99
lines changed

src/Moryx.Cli.Template/Extensions/ListExtensions.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
{
33
public static class ListExtensions
44
{
5-
public static void Each<T>(this IEnumerable<T> e, Action<T, int> action)
5+
public static List<string> Intersect(this List<string> list, string filename)
66
{
7-
var i = 0;
8-
foreach (var item in e)
9-
{
10-
action(item, i++);
11-
}
7+
var whitelist = new List<string>(){
8+
filename
9+
};
10+
return list
11+
.Intersect(whitelist, new ListComparer())
12+
.ToList();
1213
}
1314
}
1415
}
Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace Moryx.Cli.Template.StateBaseTemplate
1+
using Microsoft.CodeAnalysis.CSharp.Syntax;
2+
3+
namespace Moryx.Cli.Template.StateBaseTemplate
24
{
35
public partial class StateBaseTemplate
46
{
@@ -7,29 +9,27 @@ public class StateDefinition
79
/// <summary>
810
/// Name of the state definition. E.g.: `StateReady`
911
/// </summary>
10-
public string Name { get; init; }
12+
public required string Name { get; init; }
1113

1214
/// <summary>
1315
/// Type of the state. E.g.: `ReadyState`
1416
/// </summary>
15-
public string Type { get; init; }
17+
public required string Type { get; init; }
1618

1719
/// <summary>
1820
/// Integer value of the state, usually incremented in 10th
1921
/// </summary>
20-
public int Value { get; init; }
22+
public required int Value { get; init; }
2123

2224
/// <summary>
2325
/// Whether it is the initial state of the state machine
2426
/// </summary>
25-
public bool IsInitial { get; init; }
27+
public required bool IsInitial { get; init; }
2628

2729
/// <summary>
28-
/// Position, where the definition was found inside the `*StateBase.cs`
30+
/// Syntax node
2931
/// </summary>
30-
public int Line { get; init; }
31-
32-
32+
public required FieldDeclarationSyntax Node { get; init; }
3333
}
3434
}
3535
}

src/Moryx.Cli.Template/StateBaseTemplate/StateBaseTemplate.cs

Lines changed: 119 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
1-
using Moryx.Cli.Template.Exceptions;
2-
using Moryx.Cli.Template.Extensions;
3-
using System.Text.RegularExpressions;
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.CodeAnalysis.CSharp;
3+
using Microsoft.CodeAnalysis.CSharp.Syntax;
4+
using Microsoft.CodeAnalysis.Formatting;
5+
using Moryx.Cli.Template.Exceptions;
46

57
namespace Moryx.Cli.Template.StateBaseTemplate
68
{
79
public partial class StateBaseTemplate
810
{
11+
private const string StateDefinitionAttributeName = "StateDefinition";
12+
private const string IsInitialParameterName = "IsInitial";
913
private readonly string _content;
14+
private readonly SyntaxTree _syntaxTree;
1015

1116
public StateBaseTemplate(string content)
1217
{
1318
_content = content;
14-
Parse(content);
19+
_syntaxTree = CSharpSyntaxTree.ParseText(_content);
20+
21+
Parse(_syntaxTree);
22+
1523
}
1624

17-
public IEnumerable<Constructor> Constructors { get; private set; } = new List<Constructor>();
18-
public IEnumerable<StateDefinition> StateDefinitions { get; private set; } = new List<StateDefinition>();
25+
public IEnumerable<ConstructorDeclarationSyntax> Constructors { get; private set; } = [];
26+
public IEnumerable<StateDefinition> StateDeclarations { get; private set; } = [];
1927
public string Content { get => _content; }
2028

2129
public static StateBaseTemplate FromFile(string fileName)
@@ -24,85 +32,137 @@ public static StateBaseTemplate FromFile(string fileName)
2432
return new StateBaseTemplate(content);
2533
}
2634

27-
private void Parse(string content)
35+
private void Parse(SyntaxTree syntaxTree)
2836
{
29-
var ctors = new List<Constructor>();
30-
var states = new Dictionary<int, string>();
31-
32-
content.Split(Environment.NewLine).Each((line, i) =>
33-
{
34-
35-
if (Regex.Match(line, @"public\s+\w+\(").Success)
36-
{
37-
ctors.Add(new Constructor { Line = i + 1 });
38-
}
39-
else {
40-
var match = Regex.Match(line, @"\[\s*StateDefinition\s*\(\s*typeof\s*\(\s*(\w+)");
41-
if (match.Success)
42-
{
43-
states.Add(i, match.Groups[1].Value);
44-
}
45-
}
46-
});
47-
37+
var root = syntaxTree.GetRoot();
4838

49-
StateDefinitions = ExtractStateDefinitions(states, content);
50-
Constructors = ctors;
39+
StateDeclarations = ExtractStateDefinitions(root);
40+
Constructors = ExtractConstructors(root);
5141
}
5242

53-
private IEnumerable<StateDefinition> ExtractStateDefinitions(Dictionary<int, string> states, string content)
43+
private IEnumerable<ConstructorDeclarationSyntax> ExtractConstructors(SyntaxNode root)
5444
{
45+
return root.DescendantNodes().OfType<ConstructorDeclarationSyntax>().ToList();
46+
}
47+
48+
private IEnumerable<StateDefinition> ExtractStateDefinitions(SyntaxNode root)
49+
{
5550
var result = new List<StateDefinition>();
56-
var minifiedContent = content.Replace(Environment.NewLine, "");
51+
var states = root
52+
.DescendantNodes()
53+
.OfType<FieldDeclarationSyntax>()
54+
.Where(p => p.AttributeLists.Any(a => a.Attributes.Any(attr => attr.Name.ToString() == StateDefinitionAttributeName)));
5755

5856
foreach (var state in states)
5957
{
60-
var start = minifiedContent.IndexOf($"[StateDefinition(typeof({state.Value}");
61-
var end = minifiedContent.IndexOf(";", start);
62-
var s = minifiedContent.Substring(start, end - start +1);
63-
var match = Regex.Match(s, @"(\w+)\s*=\s*(\d+)\s*");
64-
if (match.Success) {
65-
result.Add(new StateDefinition
66-
{
67-
Name = match.Groups[1].Value,
68-
Type = state.Value,
69-
Value = Convert.ToInt32(match.Groups[2].Value),
70-
IsInitial = Regex.Match(s, @"IsInitial\s*=\s*true").Success,
71-
Line = state.Key + 1,
72-
});
73-
}
58+
var attributeArguments = GetAttributeArguments(state, StateDefinitionAttributeName);
59+
var variable = state.Declaration.Variables.First();
60+
result.Add(new StateDefinition
61+
{
62+
Name = variable.Identifier.Text,
63+
Type = attributeArguments.First(a => a.Key == "").Value,
64+
Value = Convert.ToInt32(variable.Initializer?.Value.ToString()),
65+
IsInitial = attributeArguments.Any(a => a.Key == IsInitialParameterName && a.Value.ToString() == "true"),
66+
Node = state,
67+
});
68+
7469
}
70+
7571
return result;
7672
}
7773

74+
public List<KeyValuePair<string, string>> GetAttributeArguments(FieldDeclarationSyntax field, string attributeName)
75+
{
76+
var semanticModel = CSharpCompilation.Create("SemanticModelCompilation", [_syntaxTree]).GetSemanticModel(_syntaxTree);
77+
78+
return field.AttributeLists.Select(
79+
list => list.Attributes
80+
.Where(attribute => attribute.Name.ToString() == attributeName)
81+
.Select(attribute =>
82+
attribute.ArgumentList?.Arguments.Select(a =>
83+
new KeyValuePair<string, string>(
84+
a.NameEquals?.Name.Identifier.ValueText ?? "",
85+
(a.Expression).ToString())
86+
)
87+
.ToList())
88+
.FirstOrDefault()
89+
).FirstOrDefault() ?? [];
90+
}
91+
7892
public StateBaseTemplate AddState(string stateType)
7993
{
80-
if(StateDefinitions.Any(sd => sd.Type == stateType))
94+
if(StateDeclarations.Any(sd => sd.Type == $"typeof({stateType})"))
8195
{
8296
throw new StateAlreadyExistsException(stateType);
8397
}
98+
int value = NextConst(StateDeclarations);
8499

85-
var lines = _content.Split(Environment.NewLine);
86-
var newLines = new List<string>();
87-
var ctorIndex = Constructors.First().Line - 1;
88-
newLines.AddRange(lines.Take(ctorIndex));
89-
var initial = StateDefinitions.Any(sd => sd.IsInitial)
90-
? ""
91-
: ", IsInitial = true";
92-
int value = NextConst(StateDefinitions);
100+
var parameters = new List<AttributeArgumentSyntax>
101+
{
102+
SyntaxFactory.AttributeArgument(
103+
SyntaxFactory.TypeOfExpression(SyntaxFactory.ParseTypeName(stateType)))
104+
};
105+
106+
if(!StateDeclarations.Any(sd => sd.IsInitial))
107+
{
108+
parameters.Add(SyntaxFactory.AttributeArgument(
109+
SyntaxFactory.NameEquals(IsInitialParameterName),
110+
null,
111+
SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression)));
112+
113+
}
114+
115+
var attribute = SyntaxFactory.Attribute(SyntaxFactory.ParseName(StateDefinitionAttributeName))
116+
.WithArgumentList(SyntaxFactory.AttributeArgumentList(SyntaxFactory.SeparatedList(parameters)));
117+
118+
var attributeList = SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(attribute));
119+
120+
var stateDeclaration = SyntaxFactory
121+
.FieldDeclaration(SyntaxFactory.VariableDeclaration(SyntaxFactory.ParseTypeName("int"))
122+
.AddVariables(SyntaxFactory
123+
.VariableDeclarator(TypeToConst(stateType))
124+
.WithInitializer(SyntaxFactory.EqualsValueClause(
125+
SyntaxFactory.LiteralExpression(
126+
SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(value))))))
127+
.AddModifiers(SyntaxFactory.Token(SyntaxKind.ProtectedKeyword))
128+
.AddModifiers(SyntaxFactory.Token(SyntaxKind.ConstKeyword))
129+
.AddAttributeLists(attributeList);
130+
131+
var updatedRoot = InsertStateDeclaration(stateDeclaration);
132+
updatedRoot = Formatter.Format(updatedRoot, new AdhocWorkspace());
133+
return new StateBaseTemplate(updatedRoot.ToFullString());
134+
}
93135

94-
newLines.Add($" [StateDefinition(typeof({stateType}){initial})]");
95-
newLines.Add($" protected const int {TypeToConst(stateType)} = {value};");
96-
newLines.Add("");
136+
private SyntaxNode InsertStateDeclaration(FieldDeclarationSyntax fieldDeclaration)
137+
{
138+
var root = _syntaxTree.GetRoot();
139+
var classDeclaration = root.DescendantNodes().OfType<ClassDeclarationSyntax>().FirstOrDefault();
140+
SyntaxNode? updatedClassDeclaration = null;
97141

98-
newLines.AddRange(lines.TakeLast(lines.Count() - ctorIndex));
142+
if (classDeclaration != null)
143+
{
144+
var constructor = Constructors.FirstOrDefault();
145+
if (constructor != null)
146+
{
147+
var members = classDeclaration.Members.Insert(classDeclaration.Members.IndexOf(constructor), fieldDeclaration);
148+
updatedClassDeclaration = classDeclaration.WithMembers(members);
149+
}
150+
else
151+
{
152+
updatedClassDeclaration = classDeclaration.AddMembers(fieldDeclaration);
153+
}
99154

100-
return new StateBaseTemplate(string.Join(Environment.NewLine, newLines));
155+
if (updatedClassDeclaration != null)
156+
{
157+
return root.ReplaceNode(classDeclaration, updatedClassDeclaration);
158+
}
159+
}
160+
return root;
101161
}
102162

103163
private int NextConst(IEnumerable<StateDefinition> stateDefinitions)
104164
{
105-
var result = StateDefinitions
165+
var result = StateDeclarations
106166
.OrderByDescending(sd => sd.Value)
107167
.FirstOrDefault()?
108168
.Value ?? 0;

src/Moryx.Cli.Template/StateTemplate/StateTemplate.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@ public StateTemplate ImplementIStateContext(string resource)
5050
root = CSharpSyntaxTree.ParseText(root.ToFullString()).GetRoot();
5151

5252
var usingDirective = SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(" Moryx.StateMachines"));
53-
if (!(root as CompilationUnitSyntax).Usings.Any(u => u.Name.ToString() == usingDirective.Name.ToString()))
53+
if (!((CompilationUnitSyntax)root).Usings.Any(u => u.Name?.ToString() == usingDirective.Name?.ToString()))
5454
{
55-
root = (root as CompilationUnitSyntax).AddUsings(usingDirective);
56-
root = (root as CompilationUnitSyntax).WithUsings(SyntaxFactory.List((root as CompilationUnitSyntax).Usings.OrderBy(u => u.Name?.ToString())));
55+
root = ((CompilationUnitSyntax)root).AddUsings(usingDirective);
56+
root = ((CompilationUnitSyntax)root).WithUsings(SyntaxFactory.List(((CompilationUnitSyntax)root).Usings.OrderBy(u => u.Name?.ToString())));
5757
root = CSharpSyntaxTree.ParseText(root.ToFullString()).GetRoot();
5858
}
5959

src/Moryx.Cli.Template/Template.cs

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Moryx.Cli.Template.Models;
1+
using Moryx.Cli.Template.Extensions;
2+
using Moryx.Cli.Template.Models;
23
using System.Diagnostics.CodeAnalysis;
34

45

@@ -93,16 +94,6 @@ public static List<string> Product(this List<string> list)
9394
.ToList();
9495
}
9596

96-
public static List<string> Intersect(this List<string> list, string filename)
97-
{
98-
var whitelist = new List<string>(){
99-
filename
100-
};
101-
return list
102-
.Intersect(whitelist, new ListComparer())
103-
.ToList();
104-
}
105-
10697
public static List<string> StateBaseFile(this List<string> list)
10798
=> list.Intersect("StateBase.cs");
10899

src/Tests/Moryx.Cli.Tests/CommandTests/CreateNewTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public void CheckInitialProjectFilesCount()
2020

2121
Assert.Multiple(() =>
2222
{
23-
filteredNames.Each((s, i) =>
23+
filteredNames.ForEach(s =>
2424
{
2525
Assert.That(s, Does.Not.Contain("MyResource"));
2626
Assert.That(s, Does.Not.Contain("State.cs"));

src/Tests/Moryx.Cli.Tests/StateBaseTests/EmptyStateBaseTests.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using Microsoft.CodeAnalysis;
12
using Moryx.Cli.Template.Exceptions;
23
using Moryx.Cli.Template.Extensions;
34
using Moryx.Cli.Template.StateBaseTemplate;
@@ -18,7 +19,7 @@ public void Setup()
1819
public void FirstStateShouldBeInitialState()
1920
{
2021
var newStateBase = _sut.AddState("ReadyState");
21-
var definition = newStateBase.StateDefinitions.Single();
22+
var definition = newStateBase.StateDeclarations.Single();
2223

2324
Assert.That(definition.IsInitial, Is.True);
2425
}
@@ -42,7 +43,7 @@ public void FirstStateConstIs_10()
4243
{
4344
var newStateBase = _sut.AddState("ReadyState");
4445

45-
Assert.That(newStateBase.StateDefinitions.First().Value, Is.EqualTo(10));
46+
Assert.That(newStateBase.StateDeclarations.First().Value, Is.EqualTo(10));
4647
}
4748

4849
[Test]
@@ -51,7 +52,7 @@ public void SecondStateConstIs_20()
5152
_sut = _sut.AddState("ReadyState");
5253
_sut = _sut.AddState("BusyState");
5354

54-
Assert.That(_sut.StateDefinitions.Last().Value, Is.EqualTo(20));
55+
Assert.That(_sut.StateDeclarations.Last().Value, Is.EqualTo(20));
5556
}
5657

5758
[Test]
@@ -66,14 +67,17 @@ public void ConstructorFound()
6667
{
6768
var ctor = _sut.Constructors.First();
6869
Assert.NotNull(ctor);
69-
Assert.That(ctor.Line, Is.EqualTo(7));
70+
Assert.That(GetLine(ctor.GetLocation()), Is.EqualTo(7));
7071

7172
}
7273

7374
[Test]
7475
public void ShouldInitiallyHaveNoStateDefinitions()
7576
{
76-
Assert.That(_sut.StateDefinitions.Count, Is.EqualTo(0));
77+
Assert.That(_sut.StateDeclarations.Count, Is.EqualTo(0));
7778
}
79+
80+
protected int GetLine(Location? location)
81+
=> (location?.GetLineSpan().StartLinePosition.Line ?? 0) + 1;
7882
}
7983
}

0 commit comments

Comments
 (0)