Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Source generator POC #65

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# [vNext]
# v1.0.2-local - 2024-02-22

* Support for PriorityAttribute in MsTest adapter

Expand Down
22 changes: 22 additions & 0 deletions Reqnroll.FeatureSourceGenerator/AttributeDescriptor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Collections.Immutable;

namespace Reqnroll.FeatureSourceGenerator;

public record AttributeDescriptor(
string TypeName,
string Namespace,
ImmutableArray<object?> Arguments,
ImmutableArray<KeyValuePair<string, object?>> PropertyValues)
{
public AttributeDescriptor(
string TypeName,
string Namespace,
ImmutableArray<object?>? Arguments = null,
ImmutableArray<KeyValuePair<string, object?>>? PropertyValues = null) : this(
TypeName,
Namespace,
Arguments ?? ImmutableArray<object?>.Empty,
PropertyValues ?? ImmutableArray<KeyValuePair<string, object?>>.Empty)
{
}
}
10 changes: 10 additions & 0 deletions Reqnroll.FeatureSourceGenerator/BuiltInTestFrameworkHandlers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Reqnroll.FeatureSourceGenerator;

internal static class BuiltInTestFrameworkHandlers
{
public static NUnitHandler NUnit { get; } = new();

public static MSTestHandler MSTest { get; } = new();

public static XUnitHandler XUnit { get; } = new();
}
219 changes: 219 additions & 0 deletions Reqnroll.FeatureSourceGenerator/CSharpSourceBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
using Microsoft.CodeAnalysis.Text;
using System.Text;

namespace Reqnroll.FeatureSourceGenerator;

public class CSharpSourceBuilder
{
private static readonly UTF8Encoding Encoding = new(false);

private readonly StringBuilder _buffer = new();

private bool _isFreshLine = true;

private const string Indent = " ";

private CodeBlock _context = new();

/// <summary>
/// Gets the depth of the current block.
/// </summary>
public int Depth => _context.Depth;

public CSharpSourceBuilder Append(char c)
{
AppendIndentIfIsFreshLine();

_buffer.Append(c);
return this;
}

public CSharpSourceBuilder Append(string text)
{
AppendIndentIfIsFreshLine();

_buffer.Append(text);
return this;
}

public CSharpSourceBuilder AppendConstantList(IEnumerable<object?> values)
{
var first = true;

foreach (var value in values)
{
if (first)
{
first = false;
}
else
{
Append(", ");
}

AppendConstant(value);
}

return this;
}

public CSharpSourceBuilder AppendConstant(object? value)
{
return value switch
{
null => Append("null"),
string s => AppendConstant(s),
_ => throw new NotSupportedException($"Values of type {value.GetType().FullName} cannot be encoded as a constant in C#.")
};
}

private CSharpSourceBuilder AppendConstant(string? s)
{
if (s == null)
{
return Append("null");
}

_buffer.Append('"').Append(s).Append('"');
return this;
}

private void AppendIndentIfIsFreshLine()
{
if (_isFreshLine)
{
AppendIndentToDepth();

_isFreshLine = false;
}
}

private void AppendIndentToDepth()
{
for (var i = 0; i < Depth; i++)
{
_buffer.Append(Indent);
}
}

internal void Reset() => _buffer.Clear();

public CSharpSourceBuilder AppendDirective(string directive)
{
if (!_isFreshLine)
{
throw new InvalidOperationException(ExceptionMessages.CSharpSourceBuilderCannotAppendDirectiveUnlessAtStartOfLine);
}

_buffer.Append(directive);

_buffer.AppendLine();

_isFreshLine = true;
return this;
}

public CSharpSourceBuilder AppendLine()
{
AppendIndentIfIsFreshLine();

_buffer.AppendLine();

_isFreshLine = true;
return this;
}

public CSharpSourceBuilder AppendLine(string text)
{
AppendIndentIfIsFreshLine();

_buffer.AppendLine(text);

_isFreshLine = true;
return this;
}

/// <summary>
/// Starts a new code block.
/// </summary>
public CSharpSourceBuilder BeginBlock()
{
AppendLine();
_context = new CodeBlock(_context);
return this;
}

/// <summary>
/// Appends the specified text and starts a new block.
/// </summary>
/// <param name="text">The text to append.</param>
public CSharpSourceBuilder BeginBlock(string text) => Append(text).BeginBlock();

/// <summary>
/// Ends the current block and begins a new line.
/// </summary>
/// <exception cref="InvalidOperationException">
/// <para>The builder is not currently in a block (the <see cref="Depth"/> is zero.)</para>
/// </exception>
public CSharpSourceBuilder EndBlock()
{
if (_context.Parent == null)
{
throw new InvalidOperationException(ExceptionMessages.CSharpSourceBuilderNotInCodeBlock);
}

_context = _context.Parent;
return AppendLine();
}

/// <summary>
/// Ends the current block, appends the specified text and begins a new line.
/// </summary>
/// <param name="text">The text to append.</param>
/// <exception cref="InvalidOperationException">
/// <para>The builder is not currently in a block (the <see cref="Depth"/> is zero.)</para>
/// </exception>
public CSharpSourceBuilder EndBlock(string text)
{
if (_context.Parent == null)
{
throw new InvalidOperationException(ExceptionMessages.CSharpSourceBuilderNotInCodeBlock);
}

_context = _context.Parent;
return AppendLine(text);
}

/// <summary>
/// Gets the value of this instance as a string.
/// </summary>
/// <returns>A string containing all text appended to the builder.</returns>
public override string ToString() => _buffer.ToString();

private class CodeBlock
{
public CodeBlock()
{
Depth = 0;
}

public CodeBlock(CodeBlock parent)
{
Parent = parent;
Depth = parent.Depth + 1;
}

public int Depth { get; }

public CodeBlock? Parent { get; }

public bool InSection { get; set; }

public bool HasSection { get; set; }
}

public SourceText ToSourceText()
{
return SourceText.From(_buffer.ToString(), Encoding);
}
}
79 changes: 79 additions & 0 deletions Reqnroll.FeatureSourceGenerator/CSharpSyntax.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System.Globalization;
using System.Text;

namespace Reqnroll.FeatureSourceGenerator;

internal static partial class CSharpSyntax
{
public static string CreateIdentifier(string s)
{
var sb = new StringBuilder();
var newWord = true;

foreach (char c in s)
{
if (char.IsWhiteSpace(c))
{
newWord = true;
continue;
}

if (!IsValidInIdentifier(c))
{
continue;
}

if (sb.Length == 0 && !IsValidAsFirstCharacterInIdentifier(c))
{
sb.Append('_');
sb.Append(c);
continue;
}

if (newWord)
{
sb.Append(char.ToUpper(c));
newWord = false;
}
else
{
sb.Append(c);
}
}

return sb.ToString();
}

private static bool IsValidAsFirstCharacterInIdentifier(char c)
{
if (c == '_')
{
return true;
}

var category = char.GetUnicodeCategory(c);

return category == UnicodeCategory.UppercaseLetter
|| category == UnicodeCategory.LowercaseLetter
|| category == UnicodeCategory.TitlecaseLetter
|| category == UnicodeCategory.ModifierLetter
|| category == UnicodeCategory.OtherLetter;
}

private static bool IsValidInIdentifier(char c)
{
var category = char.GetUnicodeCategory(c);

return category == UnicodeCategory.UppercaseLetter
|| category == UnicodeCategory.LowercaseLetter
|| category == UnicodeCategory.TitlecaseLetter
|| category == UnicodeCategory.ModifierLetter
|| category == UnicodeCategory.OtherLetter
|| category == UnicodeCategory.LetterNumber
|| category == UnicodeCategory.NonSpacingMark
|| category == UnicodeCategory.SpacingCombiningMark
|| category == UnicodeCategory.DecimalDigitNumber
|| category == UnicodeCategory.ConnectorPunctuation
|| category == UnicodeCategory.Format;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.CodeAnalysis;
using System.Collections.Immutable;

namespace Reqnroll.FeatureSourceGenerator;

[Generator(LanguageNames.CSharp)]
public class CSharpTestFixtureSourceGenerator : TestFixtureSourceGenerator
{
public CSharpTestFixtureSourceGenerator()
{
}

internal CSharpTestFixtureSourceGenerator(ImmutableArray<ITestFrameworkHandler> handlers) : base(handlers)
{
}
}
Loading