Skip to content

Commit

Permalink
Generate ToString methods on fixed-length char arrays
Browse files Browse the repository at this point in the history
  • Loading branch information
jnm2 committed Jun 15, 2021
1 parent d62a1b1 commit ae88117
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 1 deletion.
88 changes: 88 additions & 0 deletions src/Microsoft.Windows.CsWin32/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,19 @@ public class Generator : IDisposable
/// Gets a ref to an individual element of the inline array.
/// ⚠ Important ⚠: When this struct is on the stack, do not let the returned reference outlive the stack frame that defines it.
/// </summary>
");

private static readonly SyntaxTriviaList InlineCharArrayToStringComment = ParseLeadingTrivia(@"/// <summary>
/// Copies the fixed array to a new string, stopping before the first null terminator character or at the end of the fixed array (whichever is shorter).
/// </summary>
");

private static readonly SyntaxTriviaList InlineCharArrayToStringWithLengthComment = ParseLeadingTrivia(@"/// <summary>
/// Copies the fixed array to a new string up to the specified length regardless of whether there are null terminating characters.
/// </summary>
/// <exception cref=""ArgumentOutOfRangeException"">
/// Thrown when <paramref name=""length""/> is less than <c>0</c> or greater than <see cref=""Length""/>.
/// </exception>
");

private static readonly XmlTextSyntax DocCommentStart = XmlText(" ").WithLeadingTrivia(DocumentationCommentExterior("///"));
Expand Down Expand Up @@ -4351,6 +4364,81 @@ private ParameterSyntax CreateParameter(TypeHandleInfo parameterInfo, Parameter
.WithLeadingTrivia(InlineArrayUnsafeAsSpanComment));
}

if (elementType is PredefinedTypeSyntax { Keyword: { RawKind: (int)SyntaxKind.CharKeyword } })
{
// ...
// public string ToString(int length)
// {
// if (length < 0 || length > Length)
// throw new ArgumentOutOfRangeException(nameof(length), length, "Length must be between 0 and the fixed array length.");
// unsafe
// {
// fixed (char* p0 = &_0)
// return new string(p0, 0, length);
// }
// }
fixedLengthStruct = fixedLengthStruct.AddMembers(
MethodDeclaration(PredefinedType(Token(SyntaxKind.StringKeyword)), nameof(this.ToString))
.AddModifiers(Token(this.Visibility))
.AddParameterListParameters(
Parameter(Identifier("length")).WithType(PredefinedType(Token(SyntaxKind.IntKeyword))))
.WithBody(Block(
IfStatement(
BinaryExpression(
SyntaxKind.LogicalOrExpression,
BinaryExpression(SyntaxKind.LessThanExpression, IdentifierName("length"), LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(0))),
BinaryExpression(SyntaxKind.GreaterThanExpression, IdentifierName("length"), IdentifierName("Length"))),
ThrowStatement(ObjectCreationExpression(IdentifierName(nameof(ArgumentOutOfRangeException))).AddArgumentListArguments(
Argument(InvocationExpression(IdentifierName("nameof")).AddArgumentListArguments(
Argument(IdentifierName("length")))),
Argument(IdentifierName("length")),
Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("Length must be between 0 and the fixed array length.")))))),
UnsafeStatement(Block(FixedStatement(
VariableDeclaration(PointerType(PredefinedType(Token(SyntaxKind.CharKeyword)))).AddVariables(
VariableDeclarator(Identifier("p0")).WithInitializer(EqualsValueClause(
PrefixUnaryExpression(SyntaxKind.AddressOfExpression, IdentifierName("_0"))))),
ReturnStatement(
ObjectCreationExpression(PredefinedType(Token(SyntaxKind.StringKeyword))).AddArgumentListArguments(
Argument(IdentifierName("p0")),
Argument(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(0))),
Argument(IdentifierName("length")))))))))
.WithLeadingTrivia(InlineCharArrayToStringWithLengthComment));

if (this.canCallCreateSpan)
{
// ...
// public override string ToString()
// {
// var terminatorIndex = AsSpan().IndexOf('\0');
// return ToString(terminatorIndex != -1 ? terminatorIndex : 4);
// }
fixedLengthStruct = fixedLengthStruct.AddMembers(
MethodDeclaration(PredefinedType(Token(SyntaxKind.StringKeyword)), nameof(this.ToString))
.AddModifiers(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.OverrideKeyword))
.WithBody(Block(
LocalDeclarationStatement(VariableDeclaration(IdentifierName("var")).AddVariables(
VariableDeclarator("terminatorIndex").WithInitializer(EqualsValueClause(
InvocationExpression(MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
InvocationExpression(IdentifierName("AsSpan")),
IdentifierName(nameof(MemoryExtensions.IndexOf))))
.AddArgumentListArguments(Argument(LiteralExpression(
SyntaxKind.CharacterLiteralExpression,
Literal('\0')))))))),
ReturnStatement(InvocationExpression(IdentifierName("ToString")).AddArgumentListArguments(
Argument(ConditionalExpression(
BinaryExpression(
SyntaxKind.NotEqualsExpression,
IdentifierName("terminatorIndex"),
PrefixUnaryExpression(
SyntaxKind.UnaryMinusExpression,
LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(1)))),
IdentifierName("terminatorIndex"),
lengthLiteralSyntax))))))
.WithLeadingTrivia(InlineCharArrayToStringComment));
}
}

IdentifierNameSyntax indexParamName = IdentifierName("index");
IdentifierNameSyntax p0 = IdentifierName("p0");
IdentifierNameSyntax atThis = IdentifierName("@this");
Expand Down
61 changes: 60 additions & 1 deletion test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,60 @@ internal static partial class InlineArrayIndexerExtensions
this.AssertGeneratedType("MainAVIHeader", expected, expectedIndexer);
}

[Theory, MemberData(nameof(TFMData))]
public void FixedLengthInlineCharArraysHaveToStringWithLength(string targetFramework)
{
this.compilation = this.starterCompilations[targetFramework];

const string expected = @"
/// <summary>
/// Copies the fixed array to a new string up to the specified length regardless of whether there are null terminating characters.
/// </summary>
/// <exception cref = ""ArgumentOutOfRangeException"">
/// Thrown when <paramref name = ""length""/> is less than <c>0</c> or greater than <see cref = ""Length""/>.
/// </exception>
internal string ToString(int length)
{
if (length < 0 || length > Length)
throw new ArgumentOutOfRangeException(nameof(length), length, ""Length must be between 0 and the fixed array length."");
unsafe
{
fixed (char *p0 = &_0)
return new string (p0, 0, length);
}
}";

this.AssertGeneratedMember(
"RM_PROCESS_INFO",
type => type
.GetTypeMembers("__char_64").Single()
.GetMembers("ToString").Single(m => m is IMethodSymbol { Parameters: { Length: 1 } }),
expected);
}

[Fact]
public void FixedLengthInlineCharArraysHaveToStringOverride()
{
this.compilation = this.starterCompilations["net5.0"];

const string expected = @"
/// <summary>
/// Copies the fixed array to a new string, stopping before the first null terminator character or at the end of the fixed array (whichever is shorter).
/// </summary>
public override string ToString()
{
var terminatorIndex = AsSpan().IndexOf('\0');
return ToString(terminatorIndex != -1 ? terminatorIndex : 64);
}";

this.AssertGeneratedMember(
"RM_PROCESS_INFO",
type => type
.GetTypeMembers("__char_64").Single()
.GetMembers("ToString").Single(m => m is IMethodSymbol { Parameters: { Length: 0 } }),
expected);
}

[Theory, PairwiseData]
public void FullGeneration(bool allowMarshaling, [CombinatorialValues(Platform.AnyCpu, Platform.X86, Platform.X64, Platform.Arm64)] Platform platform)
{
Expand Down Expand Up @@ -924,14 +978,19 @@ private void AssertGeneratedType(string apiName, string expectedSyntax, string?
}

private void AssertGeneratedMember(string apiName, string memberName, string expectedSyntax)
{
this.AssertGeneratedMember(apiName, type => Assert.Single(type.GetMembers(memberName)), expectedSyntax);
}

private void AssertGeneratedMember(string apiName, Func<INamedTypeSymbol, ISymbol> memberSelector, string expectedSyntax)
{
this.generator = new Generator(this.metadataStream, DefaultTestGeneratorOptions, this.compilation, this.parseOptions);
Assert.True(this.generator.TryGenerate(apiName, CancellationToken.None));
this.CollectGeneratedCode(this.generator);
this.AssertNoDiagnostics();
BaseTypeDeclarationSyntax typeSyntax = Assert.Single(this.FindGeneratedType(apiName));
var semanticModel = this.compilation.GetSemanticModel(typeSyntax.SyntaxTree, ignoreAccessibility: false);
var member = Assert.Single(semanticModel.GetDeclaredSymbol(typeSyntax, CancellationToken.None)!.GetMembers(memberName));
var member = memberSelector.Invoke(semanticModel.GetDeclaredSymbol(typeSyntax, CancellationToken.None)!);
var memberSyntax = member.DeclaringSyntaxReferences.Single().GetSyntax(CancellationToken.None);
Assert.Equal(
TestUtils.NormalizeToExpectedLineEndings(expectedSyntax).Trim(),
Expand Down

0 comments on commit ae88117

Please sign in to comment.