Skip to content
Merged
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
16 changes: 16 additions & 0 deletions docs/code-writer.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ writer.MethodCall("Create", ["x"], receiver: "factory", genericArguments: [TypeR
// factory.Create<string>(x);
```

When a statement or declaration must embed a runtime or user-supplied string — for example a
regular-expression pattern or error message — emit it through the `StringLiteral()` extension rather
than wrapping it in quotes by hand. It returns a quoted, escaped C# string literal:

```csharp
body.Field("regex", regexType, TypeDeclarationAccessibility.Private,
options => options with { IsStatic = true, Initializer = $"new({pattern.StringLiteral()})" });
// pattern = ^[\w\-.]+$ => new("^[\\w\\-.]+$")
```

A **chained** invocation — where the result of each call is the receiver of the next, and a postfix is
applied to the final result — is expressed with `MethodCallChain`/`AwaitedMethodCallChain`. The chain
is written as an expression (no terminating semicolon), so it composes as the value of an
Expand Down Expand Up @@ -279,6 +289,12 @@ Emits:
namespace Purview.Telemetry;
```

The generator version in the header and the `GeneratedCode` attribute comes from the
`GenerationSettings` used to create the writer. When settings are created via
`GenerationSettings.Create<TGenerator>()`, the full assembly informational version is used, so any
pre-release suffix (such as `-alpha`) and build metadata (such as `+commit-hash`) are preserved rather
than being reduced to the numeric assembly version.

### Conditional compilation returns

`NetConditionalReturn` writes a `return` for an interpolated string using the best invariant-culture
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "purview-sourcegeneratorframework",
"version": "1.0.0-prerelease.35",
"version": "1.0.0-prerelease.36",
"private": true
}
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,12 @@ static void EmitServiceInfo(SourceProductionContext spc, ServiceRegistrationGene
"Name",
TypeIdentity.Create<string>().AsTypeReference(),
TypeDeclarationAccessibility.Public,
options => options with { IsStatic = true, ExpressionBody = $"\"{target.Name}\"" }
options =>
options with
{
IsStatic = true,
ExpressionBody = target.Name.StringLiteral(),
}
);

inner.Property(
Expand All @@ -173,7 +178,7 @@ static void EmitServiceInfo(SourceProductionContext spc, ServiceRegistrationGene
options with
{
IsStatic = true,
ExpressionBody = $"\"{target.LifetimeMemberName}\"",
ExpressionBody = target.LifetimeMemberName.StringLiteral(),
}
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel;
using Microsoft.CodeAnalysis.CSharp;

namespace System;

Expand All @@ -15,6 +16,15 @@ public static class StringExtension
/// <returns>The surrounded string.</returns>
public string Surround(string surroundWith = "\"") => $"{surroundWith}{value}{surroundWith}";

/// <summary>
/// Returns a quoted, escaped C# string literal for the value so it can be safely emitted into
/// generated source. Backslashes, quotes, and other characters requiring escaping are escaped;
/// e.g. <c>^[\w\-.]+$</c> becomes <c>"^[\\w\\-.]+$"</c>. A null value is emitted as the
/// <c>null</c> keyword.
/// </summary>
/// <returns>The value as a C# string literal, or the <c>null</c> keyword if the value is null.</returns>
public string StringLiteral() => value is null ? "null" : SymbolDisplay.FormatLiteral(value, true);

/// <summary>
/// Returns the string value or "null" if the value is null. If <paramref name="useWhitespaceCheck"/> is true, then it will also return "null" if the value is whitespace.
/// </summary>
Expand Down
21 changes: 15 additions & 6 deletions src/src/SourceGeneratorShared/GenerationSettings.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Reflection;
using Microsoft.CodeAnalysis.CSharp;

namespace Purview.SourceGeneratorFramework;
Expand Down Expand Up @@ -171,7 +172,10 @@ public GenerationSettings(
TypeDeclarationAccessibility.Public;

/// <summary>
/// Creates a new generation settings instance for the specified generator type, using the type name and assembly version.
/// Creates a new generation settings instance for the specified generator type, using the type name and
/// the assembly's informational version. The informational version carries the full SemVer details,
/// including any pre-release suffix (such as <c>-alpha</c>) and build metadata (such as <c>+hash</c>),
/// which are not present in the numeric assembly version.
/// </summary>
/// <typeparam name="TGenerator">The type of the generator.</typeparam>
/// <param name="disabledSourceGenMSBuildProperty">An optional MSBuild property name that disables the generator when set to true.</param>
Expand All @@ -180,10 +184,15 @@ public static GenerationSettings Create<TGenerator>(string? disabledSourceGenMSB
{
var generatorType = typeof(TGenerator);

return new(
generatorType.Name,
generatorType.Assembly.GetName().Version?.ToString(),
disabledSourceGenMSBuildProperty
);
return new(generatorType.Name, GetGeneratorVersion(generatorType.Assembly), disabledSourceGenMSBuildProperty);
}

/// <summary>
/// Gets the full version for an assembly, preferring the informational version (which includes any
/// pre-release suffix and build metadata) and falling back to the numeric assembly version.
/// </summary>
static string GetGeneratorVersion(Assembly assembly) =>
assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
?? assembly.GetName().Version?.ToString()
?? "1.0.0.0";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Purview.SourceGeneratorFramework;

public class StringExtensionTests
{
[Test]
[Arguments("Hello, World!", "\"Hello, World!\"")]
[Arguments(@"^[\w\-.]+$", "\"^[\\\\w\\\\-.]+$\"")]
[Arguments("say \"hi\"", "\"say \\\"hi\\\"\"")]
[Arguments("", "\"\"")]
[Arguments("a\nb", "\"a\\nb\"")]
[Arguments("a\tb", "\"a\\tb\"")]
[Arguments("a\r\nb", "\"a\\r\\nb\"")]
public async Task StringLiteral_GivenValue_EscapesItAsCSharpStringLiteral(string value, string expected)
{
await Assert.That(value.StringLiteral()).IsEqualTo(expected);
}

[Test]
public async Task StringLiteral_GivenNull_ReturnsNullKeyword()
{
await Assert.That(((string?)null).StringLiteral()).IsEqualTo("null");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Reflection;
using Purview.SourceGeneratorFramework.TestGenerators;

namespace Purview.SourceGeneratorFramework;

public class GenerationSettingsTests
{
[Test]
public async Task Create_GivenGeneratorType_UsesFullInformationalVersion()
{
var assembly = typeof(AlwaysNullableContextTestGenerator).Assembly;
var informationalVersion = assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
?.InformationalVersion;

await Assert.That(informationalVersion).IsNotNull();

var settings = GenerationSettings.Create<AlwaysNullableContextTestGenerator>();

await Assert.That(settings.GeneratorVersion).IsEqualTo(informationalVersion);
await Assert.That(settings.GeneratorVersion).IsNotEqualTo(assembly.GetName().Version?.ToString());
}
}