Skip to content

Commit

Permalink
Enhancements and fixes in source generators
Browse files Browse the repository at this point in the history
- Added a new source generator `ConfigurationKeysGenerator` using Roslyn APIs to dynamically generate configuration key classes for annotated classes, including logic for class identification and source code output.
- Enhanced documentation in `LiteralGenerator.cs` by adding XML comments to key methods and operators, improving code readability and understanding.
- Refined attribute handling in `ParsedGenerator.cs` to include null checks, enhancing code robustness.
- Updated `Microsoft.CodeAnalysis.CSharp` to version `4.10.0` in `Radix.Generators.csproj` and commented out an unused property for future use.
- Extended `StringExtensions` with a method for capitalizing the first character of a string.
- Improved documentation and parameter annotation in `ValidatedGenerator.cs`, ensuring completeness and correctness.
  • Loading branch information
MCGPPeters committed Jun 20, 2024
1 parent aafb736 commit 70ed13f
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 23 deletions.
104 changes: 104 additions & 0 deletions src/Radix.Generators/ConfigurationKeysGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@

using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;
using System.Diagnostics;

namespace Radix.Generators;


[Generator]
public class ConfigurationKeysGenerator : IIncrementalGenerator

Check warning on line 13 in src/Radix.Generators/ConfigurationKeysGenerator.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Missing XML comment for publicly visible type or member 'ConfigurationKeysGenerator'

Check warning on line 13 in src/Radix.Generators/ConfigurationKeysGenerator.cs

View workflow job for this annotation

GitHub Actions / Analyze (javascript)

Missing XML comment for publicly visible type or member 'ConfigurationKeysGenerator'
{
public void Initialize(IncrementalGeneratorInitializationContext context)

Check warning on line 15 in src/Radix.Generators/ConfigurationKeysGenerator.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Missing XML comment for publicly visible type or member 'ConfigurationKeysGenerator.Initialize(IncrementalGeneratorInitializationContext)'

Check warning on line 15 in src/Radix.Generators/ConfigurationKeysGenerator.cs

View workflow job for this annotation

GitHub Actions / Analyze (javascript)

Missing XML comment for publicly visible type or member 'ConfigurationKeysGenerator.Initialize(IncrementalGeneratorInitializationContext)'
{
// Debugger.Launch();
var classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: (syntaxNode, _) => syntaxNode is ClassDeclarationSyntax classDecl && classDecl.AttributeLists.Count > 0,
transform: (syntaxContext, _) => (classDecl: (ClassDeclarationSyntax)syntaxContext.Node, model: syntaxContext.SemanticModel))
.Where(pair => HasConfigurationAttribute(pair.classDecl, pair.model));

var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect());

context.RegisterSourceOutput(compilationAndClasses, static (sourceProductionContext, source) =>
{
var (compilation, classes) = source;
foreach (var (classSyntax, model) in classes.Distinct())
{
var className = classSyntax.Identifier.Text;
var typeSymbol = model.GetDeclaredSymbol(classSyntax);
var namespaceName = typeSymbol?.ContainingNamespace.ToDisplayString() ?? "global";
var configurationKeysClassName = className + "ConfigurationKeys";
var hintName = $"{configurationKeysClassName}_{Guid.NewGuid()}.g.cs";
var sourceCode = GenerateConfigurationKeysClass(compilation, namespaceName, className, configurationKeysClassName, classSyntax, model);
sourceProductionContext.AddSource(hintName, SourceText.From(sourceCode, Encoding.UTF8));
}
});
}

private static bool HasConfigurationAttribute(ClassDeclarationSyntax classSyntax, SemanticModel model)
{
var classSymbol = model.GetDeclaredSymbol(classSyntax);
return classSymbol is not null ? classSymbol.GetAttributes().Any(ad => ad.AttributeClass is not null ? ad.AttributeClass.ToDisplayString() == "Radix.ConfigurationAttribute" : false) : false;
}

private static string GenerateConfigurationKeysClass(Compilation compilation, string namespaceName, string className, string configurationKeysClassName, ClassDeclarationSyntax classSyntax, SemanticModel model)
{
var stringBuilder = new StringBuilder();
// Header comment indicating the code is auto-generated
stringBuilder.AppendLine("// <auto-generated>");
stringBuilder.AppendLine("// This code was generated by a tool.");
stringBuilder.AppendLine("// Changes to this file may cause incorrect behavior and will be lost if");
stringBuilder.AppendLine("// the code is regenerated.");
stringBuilder.AppendLine("// </auto-generated>");
stringBuilder.AppendLine();
stringBuilder.AppendLine("using System;");
stringBuilder.AppendLine("using System.Diagnostics;");
stringBuilder.AppendLine("using System.CodeDom.Compiler;"); // Include namespace for GeneratedCodeAttribute
stringBuilder.AppendLine();
stringBuilder.AppendLine($"namespace {namespaceName}");
stringBuilder.AppendLine("{");
// Apply DebuggerNonUserCode and GeneratedCodeAttribute to the class
stringBuilder.AppendLine(" [DebuggerNonUserCode]");
stringBuilder.AppendLine(" [GeneratedCode(\"Radix.Generators.ConfigurationKeysGenerator\", \"1.0\")]"); // Adjust version as appropriate
stringBuilder.AppendLine($" /// <summary>");
stringBuilder.AppendLine($" /// Provides configuration key paths for the {className} class.");
stringBuilder.AppendLine($" /// </summary>");
stringBuilder.AppendLine($" public class {configurationKeysClassName}");
stringBuilder.AppendLine(" {");

var classSymbol = model.GetDeclaredSymbol(classSyntax);

GenerateConfigurationKeysForType(stringBuilder, classSymbol!, className);

stringBuilder.AppendLine(" }");
stringBuilder.AppendLine("}");
string v = stringBuilder.ToString();
return v;
}


private static void GenerateConfigurationKeysForType(StringBuilder stringBuilder, INamedTypeSymbol classSymbol, string parentPath)
{
foreach (var property in classSymbol.GetMembers().OfType<IPropertySymbol>())
{
var propertyName = property.Name;
var propertyPath = $"{parentPath}:{propertyName}";

// Add property comment
stringBuilder.AppendLine($" /// <summary>");
stringBuilder.AppendLine($" /// Configuration key for {propertyName}.");
stringBuilder.AppendLine($" /// </summary>");
stringBuilder.AppendLine($" public static string {propertyPath.Replace(':', '_')} => \"{propertyPath}\";");

if (property.Type.TypeKind == TypeKind.Class && property.Type.SpecialType != SpecialType.System_String)
{
GenerateConfigurationKeysForType(stringBuilder, (INamedTypeSymbol)property.Type, propertyPath);
}
}
}

}
100 changes: 88 additions & 12 deletions src/Radix.Generators/LiteralGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,23 +63,63 @@ internal static string ProcessType(ISymbol typeSymbol, TypeDeclarationSyntax typ
};

var equalsOperatorsSource = $@"
public static bool operator ==({typeSymbol.ToDisplayString()} left, {typeSymbol.ToDisplayString()} right) => Equals(left, right);
public static bool operator !=({typeSymbol.ToDisplayString()} left, {typeSymbol.ToDisplayString()} right) => !Equals(left, right);
";
/// <summary>
/// Determines whether two specified instances are equal.
/// </summary>
/// <param name=""left"">The first instance to compare.</param>
/// <param name=""right"">The second instance to compare.</param>
/// <returns>true if left and right are equal; otherwise, false.</returns>
public static bool operator ==({typeSymbol.ToDisplayString()} left, {typeSymbol.ToDisplayString()} right) => Equals(left, right);
/// <summary>
/// Determines whether two specified instances are not equal.
/// </summary>
/// <param name=""left"">The first instance to compare.</param>
/// <param name=""right"">The second instance to compare.</param>
/// <returns>true if left and right are not equal; otherwise, false.</returns>
public static bool operator !=({typeSymbol.ToDisplayString()} left, {typeSymbol.ToDisplayString()} right) => !Equals(left, right);
";

var equalsSource = typeDeclarationSyntax.Kind() switch
{
SyntaxKind.ClassDeclaration => $@"
{equalsOperatorsSource}
public override bool Equals(object obj) => obj is {typeSymbol.ToDisplayString()} other;
public override int GetHashCode() => ""{typeSymbolName}"".GetHashCode();
public bool Equals({typeSymbolName} other) => true;",
{equalsOperatorsSource}
/// <summary>
/// Determines whether the specified object is equal to the current object.
/// </summary>
/// <param name=""obj"">The object to compare with the current object.</param>
/// <returns>true if the specified object is equal to the current object; otherwise, false.</returns>
public override bool Equals(object obj) => obj is {typeSymbol.ToDisplayString()} other;
/// <summary>
/// Serves as the default hash function.
/// </summary>
/// <returns>A hash code for the current object.</returns>
public override int GetHashCode() => ""{typeSymbolName}"".GetHashCode();
/// <summary>
/// Indicates whether the current object is equal to another object of the same type.
/// </summary>
/// <param name=""other"">An object to compare with this object.</param>
/// <returns>true if the current object is equal to the other parameter; otherwise, false.</returns>
public bool Equals({typeSymbolName} other) => true;",
SyntaxKind.RecordDeclaration => "",
SyntaxKind.StructDeclaration => $@"
{equalsOperatorsSource}
public override bool Equals(object? obj) => obj is {typeSymbol.ToDisplayString()} other;
public override int GetHashCode() => ""{typeSymbolName}"".GetHashCode();
public bool Equals({typeSymbolName} other) => true;",
{equalsOperatorsSource}
/// <summary>
/// Determines whether the specified object is equal to the current object.
/// </summary>
/// <param name=""obj"">The object to compare with the current object.</param>
/// <returns>true if the specified object is equal to the current object; otherwise, false.</returns>
public override bool Equals(object? obj) => obj is {typeSymbol.ToDisplayString()} other;
/// <summary>
/// Serves as the default hash function.
/// </summary>
/// <returns>A hash code for the current object.</returns>
public override int GetHashCode() => ""{typeSymbolName}"".GetHashCode();
/// <summary>
/// Indicates whether the current object is equal to another object of the same type.
/// </summary>
/// <param name=""other"">An object to compare with this object.</param>
/// <returns>true if the current object is equal to the other parameter; otherwise, false.</returns>
public bool Equals({typeSymbolName} other) => true;",
SyntaxKind.RecordStructDeclaration => "",
_ => throw new NotSupportedException("Unsupported type kind for generating Literal code")
};
Expand All @@ -91,18 +131,54 @@ namespace {namespaceName}
{{
{kindSource}, System.IParsable<{typeSymbolName}>
{{
/// <summary>
/// Returns a string that represents the current object.
/// </summary>
/// <returns>A string that represents the current object.</returns>
public override string ToString() => ""{toString}"";
{equalsSource}
/// <summary>
/// Defines an implicit conversion of a {typeSymbolName} to a string.
/// </summary>
/// <param name=""value"">The {typeSymbolName} to convert.</param>
/// <returns>A string representation of the value.</returns>
public static implicit operator string({typeSymbolName} value) => ""{toString}"";
public static implicit operator {typeSymbolName}(string value) => value == ""{toString}"" ? new() : throw new ArgumentException(""'value' is not assignable to '{typeSymbol.Name}'"");
/// <summary>
/// Defines an implicit conversion of a string to a {typeSymbolName}.
/// </summary>
/// <param name=""value"">The string to convert.</param>
/// <returns>A {typeSymbolName} representation of the string.</returns>
public static implicit operator {typeSymbolName}(string value) => value == ""{toString}"" ? new() : throw new ArgumentException(""'value' is not assignable to '{typeSymbol.Name}'"");
/// <summary>
/// Formats the value of the current instance using the specified format.
/// </summary>
/// <returns>The value of the current instance in string format.</returns>
public static string Format() => ""{toString}"";
public string ToString(string? format, IFormatProvider? formatProvider) => ""{toString}"";
/// <summary>
/// Converts the string representation of a number to its {typeSymbolName} equivalent.
/// </summary>
/// <param name=""s"">A string containing a number to convert.</param>
/// <param name=""provider"">An object that supplies culture-specific formatting information.</param>
/// <returns>A {typeSymbolName} equivalent to the number contained in s.</returns>
public static {typeSymbolName} Parse(string s, IFormatProvider? provider)
{{
if (""{toString}"" == s) return new {typeSymbolName}();
throw new ArgumentException(""'value' is not assignable to '{typeSymbol.Name}'"");
}}
/// <summary>
/// Tries to convert the string representation of a number to its {typeSymbolName} equivalent, and returns a value that indicates whether the conversion succeeded.
/// </summary>
/// <param name=""s"">The string representation of a number.</param>
/// <param name=""provider"">An object that supplies culture-specific formatting information.</param>
/// <param name=""result"">When this method returns, contains the {typeSymbolName} value equivalent to the number contained in s, if the conversion succeeded, or null if the conversion failed. The conversion fails if the s parameter is null or String.Empty, is not of the correct format, or represents a number less than MinValue or greater than MaxValue. This parameter is passed uninitialized; any value originally supplied in result will be overwritten.</param>
/// <returns>true if s was converted successfully; otherwise, false.</returns>
public static bool TryParse(string? s, IFormatProvider? provider, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(returnValue: false)] out {typeSymbolName} result)
{{
result = default;
Expand Down
18 changes: 10 additions & 8 deletions src/Radix.Generators/ParsedGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,16 @@ public void Execute(GeneratorExecutionContext context)
var attributes = typeSymbol!.GetAttributes().Where(attribute => attribute.AttributeClass!.Name.Equals(attributeSymbol!.Name));
foreach (var attribute in attributes)
{
Console.WriteLine($"{attribute.AttributeClass!.TypeArguments[1].ContainingNamespace.Name}.{attribute.AttributeClass.TypeArguments[1].Name}");
var classSource = ProcessType(attribute.AttributeClass.TypeArguments[0].Name, $"{attribute.AttributeClass.TypeArguments[1].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}", typeSymbol, candidate);
// fix text formating according to default ruleset
var normalizedSourceCodeText
= CSharpSyntaxTree.ParseText(classSource).GetRoot().NormalizeWhitespace().GetText(Encoding.UTF8);
context.AddSource(
$"Validated{typeSymbol.ContainingNamespace.ToDisplayString()}_{typeSymbol.Name}",
normalizedSourceCodeText);
if (attribute.AttributeClass is not null)
{
var classSource = ProcessType(attribute.AttributeClass.TypeArguments[0].Name, $"{attribute.AttributeClass.TypeArguments[1].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}", typeSymbol, candidate);
// fix text formating according to default ruleset
var normalizedSourceCodeText
= CSharpSyntaxTree.ParseText(classSource).GetRoot().NormalizeWhitespace().GetText(Encoding.UTF8);
context.AddSource(
$"Validated{typeSymbol.ContainingNamespace.ToDisplayString()}_{typeSymbol.Name}",
normalizedSourceCodeText);
}
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/Radix.Generators/Radix.Generators.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>Preview</LangVersion>
<IsRoslynComponent>true</IsRoslynComponent>
<!-- <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> -->
</PropertyGroup>

<ItemGroup>
Expand All @@ -13,7 +14,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0-beta1.24122.1" PrivateAssets="all" />
</ItemGroup>

Expand Down
3 changes: 3 additions & 0 deletions src/Radix.Generators/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ public static class StringExtensions
{
public static string FirstCharacterToLowerCase(this string s) =>
char.ToLowerInvariant(s[0]) + s.Substring(1);

public static string FirstCharacterToUpperCase(this string s) =>
char.ToUpperInvariant(s[0]) + s.Substring(1);
}
7 changes: 5 additions & 2 deletions src/Radix.Generators/ValidatedGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,10 @@ private string Selector(AttributeData attribute)
return $"{attribute.AttributeClass?.BaseType?.TypeArguments[1].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}";
}


/// <summary>
///
/// </summary>
/// <param name="context"></param>
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
Expand All @@ -105,7 +108,7 @@ public void Initialize(GeneratorInitializationContext context)
/// </summary>
/// <param name="valueTypeName">The typename of the value to validate (the aliased type)</param>
/// <param name="validityTypeNames">The type names of the validity instances holding the validator functions</param>
/// <param name="typeSymbol">The symbol of the type to which the Validated attributes were added
/// <param name="typeSymbol">The symbol of the type to which the Validated attributes were added</param>
/// <param name="typeDeclarationSyntax">The declaration syntax of the type to which the Validated attributes were added</param>
/// <returns></returns>
/// <exception cref="NotSupportedException"></exception>
Expand Down

0 comments on commit 70ed13f

Please sign in to comment.