Skip to content

Commit

Permalink
Nullable context incorporation + config (#13)
Browse files Browse the repository at this point in the history
Add nullable annotation context directive 
Add config options for nullable context Some refactoring
Add config options for nullable context
Some refactoring
  • Loading branch information
StefanOssendorf authored Feb 22, 2024
1 parent 2c44a05 commit f5df472
Show file tree
Hide file tree
Showing 93 changed files with 713 additions and 210 deletions.
14 changes: 6 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,10 @@ static partial class DataPortalExtensions {
## How to configure the generator

You can configure the following for the generator to respect
* method prefix
* method suffix
* method prefix (default = "")
* method suffix (default = "")
* Enable/Disable nullable annotation context (default = Enable)
* SuppressWarningCS8669 (default = false)

The fetch named method example from above can be resolved with a prefix/suffix to generate a method with the name `YourFetch` which in turn can be used and provides reliable compiler support.

Expand All @@ -75,6 +77,8 @@ You can add the following properties to your csproj-file to configure the genera
<PropertyGroup>
<DataPortalExtensionGen_MethodPrefix>Prefix</DataPortalExtensionGen_MethodPrefix>
<DataPortalExtensionGen_MethodSuffix>Suffix</DataPortalExtensionGen_MethodSuffix>
<DataPortalExtensionGen_NullableContext>Enable/Disable</DataPortalExtensionGen_NullableContext>
<DataPortalExtensionGen_SuppressWarningCS8669>true/false</DataPortalExtensionGen_SuppressWarningCS8669>
</PropertyGroup>
```

Expand All @@ -87,12 +91,6 @@ With this added the consuming project the generator picks the values up and adds
- Special case commands to an extension like `commandPortal.ExecuteCommand(<params>)` which combines `Create`+`Execute`.
- Support for generic business objects
- Add attribute to exclude methods explicitly
- Add proper NullableAnnotationContext settings
- Add diagnostics
- Wrong usage/configuration
- Like extension class is not partial
- Wrong config setting (if possible)
- Detailed error when a private nested class is used for any csla method

A lot of implementation details are derived/taken from the great series [Andrew Lock: Creating a source generator](https://andrewlock.net/series/creating-a-source-generator/). If you want to create your own source generator I can recommend that series wholeheartedly.

Expand Down
8 changes: 8 additions & 0 deletions src/Csla.DataPortalExtensionGenerator/ConfigConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Ossendorf.Csla.DataPortalExtensionGenerator;

internal static class ConfigConstants {
public const string MethodPrefix = "DataPortalExtensionGen_MethodPrefix";
public const string MethodSuffix = "DataPortalExtensionGen_MethodSuffix";
public const string NullableContext = "DataPortalExtensionGen_NullableContext";
public const string SuppressWarningCS8669 = "DataPortalExtensionGen_SuppressWarningCS8669";
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Ossendorf.Csla.DataPortalExtensionGenerator.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;

namespace Ossendorf.Csla.DataPortalExtensionGenerator;
Expand All @@ -22,7 +23,10 @@ private static void AddMarkerAttribute(IncrementalGeneratorInitializationContext
=> context.RegisterPostInitializationOutput(ctx => ctx.AddSource("DataPortalExtensionsAttribute.g.cs", SourceText.From(GeneratorHelper.MarkerAttribute, Encoding.UTF8)));

private static void AddCodeGenerator(IncrementalGeneratorInitializationContext context) {
var options = GetGeneratorOptions(context);
var optionsAndDiagnostics = GetGeneratorOptions(context);

var options = optionsAndDiagnostics
.Select((o, _) => o.Value);

var extensionClassesAndDiagnostics = context.SyntaxProvider
.ForAttributeWithMetadataName(
Expand Down Expand Up @@ -55,25 +59,49 @@ private static void AddCodeGenerator(IncrementalGeneratorInitializationContext c
methodDeclarationsAndDiagnostics.SelectMany((m, _) => m.Errors),
static (ctx, info) => ctx.ReportDiagnostic(info)
);
context.RegisterSourceOutput(
optionsAndDiagnostics.SelectMany((o, _) => o.Errors),
static (ctx, info) => ctx.ReportDiagnostic(info)
);

context.RegisterSourceOutput(
source: classesToGenerateInto,
action: Emitter.EmitExtensionClass
);
}

private static IncrementalValueProvider<GeneratorOptions> GetGeneratorOptions(IncrementalGeneratorInitializationContext context) {
private static IncrementalValueProvider<Result<GeneratorOptions>> GetGeneratorOptions(IncrementalGeneratorInitializationContext context) {
return context.AnalyzerConfigOptionsProvider.Select((options, _) => {

if (!options.GlobalOptions.TryGetValue("build_property.DataPortalExtensionGen_MethodPrefix", out var methodPrefix) || methodPrefix is null) {
if (!TryGetGlobalOption(ConfigConstants.MethodPrefix, out var methodPrefix) || methodPrefix is null) {
methodPrefix = "";
}

if (!options.GlobalOptions.TryGetValue("build_property.DataPortalExtensionGen_MethodSuffix", out var methodSuffix) || methodSuffix is null) {
if (!TryGetGlobalOption(ConfigConstants.MethodSuffix, out var methodSuffix) || methodSuffix is null) {
methodSuffix = "";
}

return new GeneratorOptions(methodPrefix, methodSuffix);
var errors = new List<DiagnosticInfo>();
var nullableContextOptions = NullableContextOptions.Enable;
if (TryGetGlobalOption(ConfigConstants.NullableContext, out var nullabilityContext)) {
if (nullabilityContext.Equals("Disable", StringComparison.OrdinalIgnoreCase)) {
nullableContextOptions = NullableContextOptions.Disable;
} else if (!nullabilityContext.Equals("Enable", StringComparison.OrdinalIgnoreCase)) {
errors.Add(NullableContextValueDiagnostic.Create(nullabilityContext));
}
}

var suppressWarningCS8669 = false;
if (TryGetGlobalOption(ConfigConstants.SuppressWarningCS8669, out var suppressWarningString)) {
if (!bool.TryParse(suppressWarningString, out suppressWarningCS8669)) {
suppressWarningCS8669 = false;
errors.Add(SuppressWarningCS8669ValueDiagnostic.Create(suppressWarningString));
}
}

return new Result<GeneratorOptions>(new GeneratorOptions(methodPrefix, methodSuffix, nullableContextOptions, suppressWarningCS8669), new EquatableArray<DiagnosticInfo>([.. errors]));

bool TryGetGlobalOption(string key, [NotNullWhen(true)] out string? value) => options.GlobalOptions.TryGetValue($"build_property.{key}", out value);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace Ossendorf.Csla.DataPortalExtensionGenerator.Diagnostics;
internal static class DiagnosticHelper {
public static void ReportDiagnostic(this SourceProductionContext ctx, DiagnosticInfo info)
=> ctx.ReportDiagnostic(CreateDiagnostic(info));

private static Diagnostic CreateDiagnostic(DiagnosticInfo info) {
var diagnostic = Diagnostic.Create(info.Descriptor, info.Location);
return diagnostic;
Expand Down
15 changes: 15 additions & 0 deletions src/Csla.DataPortalExtensionGenerator/Diagnostics/DiagnosticId.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using NetEscapades.EnumGenerators;

namespace Ossendorf.Csla.DataPortalExtensionGenerator.Diagnostics;

[EnumExtensions]
internal enum DiagnosticId {
// NotPartialDiagnostic
DPEGEN001,
// PrivateClassCanNotBeAParameterDiagnostic
DPEGEN002,
// NullableContextValueDiagnostic
DPEGEN003,
// SuppressWarningCS8669ValueDiagnostic
DPEGEN004,
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

namespace Ossendorf.Csla.DataPortalExtensionGenerator.Diagnostics;

internal record DiagnosticInfo(DiagnosticDescriptor Descriptor, Location Location);
internal record DiagnosticInfo(DiagnosticDescriptor Descriptor, Location? Location);
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@

namespace Ossendorf.Csla.DataPortalExtensionGenerator.Diagnostics;
internal static class NotPartialDiagnostic {
internal const string Id = "DPEGEN001";
internal const string Message = $"The target of the {GeneratorHelper.MarkerAttributeNameWithSuffix} must be declared as partial.";
internal const string Title = "Must be partial";

public static DiagnosticInfo Create(ClassDeclarationSyntax syntax)
=> new(new DiagnosticDescriptor(Id, Title, Message, "Usage", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true), syntax.GetLocation());
=> new(new DiagnosticDescriptor(DiagnosticId.DPEGEN001.ToStringFast(), Title, Message, "Usage", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true), syntax.GetLocation());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Microsoft.CodeAnalysis;
using System.Globalization;

namespace Ossendorf.Csla.DataPortalExtensionGenerator.Diagnostics;

internal static class NullableContextValueDiagnostic {
internal const string Message = $"The value '{{0}}' for setting '{ConfigConstants.NullableContext}' is not known. Only 'Enable' and 'Disable' are allowed. Default of 'Enable' is used.";
internal const string Title = "Nullable context value unknown";

public static DiagnosticInfo Create(string configValue)
=> new(new DiagnosticDescriptor(DiagnosticId.DPEGEN003.ToStringFast(), Title, string.Format(CultureInfo.InvariantCulture, Message, configValue), "Usage", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true), null);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
namespace Ossendorf.Csla.DataPortalExtensionGenerator.Diagnostics;

internal static class PrivateClassCanNotBeAParameterDiagnostic {
internal const string Id = "DPEGEN002";
internal const string Message = "The {0} method for {1} has a paramter of type {2} which is private and can't be used for generating an extension method.";
internal const string Title = "CSLA method parameters must not be private types.";

public static DiagnosticInfo Create(MethodDeclarationSyntax syntax, DataPortalMethod dataPortalMethod, string methodName, string invalidClassType)
=> new(new DiagnosticDescriptor(Id, Title, string.Format(CultureInfo.InvariantCulture, Message, dataPortalMethod.ToStringFast(), methodName, invalidClassType), "Usage", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true), syntax.GetLocation());
=> new(new DiagnosticDescriptor(DiagnosticId.DPEGEN002.ToStringFast(), Title, string.Format(CultureInfo.InvariantCulture, Message, dataPortalMethod.ToStringFast(), methodName, invalidClassType), "Usage", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true), syntax.GetLocation());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Microsoft.CodeAnalysis;
using System.Globalization;

namespace Ossendorf.Csla.DataPortalExtensionGenerator.Diagnostics;

internal static class SuppressWarningCS8669ValueDiagnostic {
internal const string Message = $"The value '{{0}}' for setting '{ConfigConstants.SuppressWarningCS8669}' is not parseable to boolean. Default of false is used.";
internal const string Title = "CS8669 value unknown";

public static DiagnosticInfo Create(string configValue)
=> new(new DiagnosticDescriptor(DiagnosticId.DPEGEN004.ToStringFast(), Title, string.Format(CultureInfo.InvariantCulture, Message, configValue), "Usage", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true), null);
}
12 changes: 3 additions & 9 deletions src/Csla.DataPortalExtensionGenerator/Emitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,15 @@ public static void EmitExtensionClass(SourceProductionContext context, ((ClassFo
}

private static string GenerateCode(in ClassForExtensions extensionClass, in ImmutableArray<PortalOperationToGenerate> methods, in GeneratorOptions options, CancellationToken ct) {

var ns = extensionClass.Namespace;
var name = extensionClass.Name;

return new StringBuilder()
.AppendLine("// <auto-generated />")

//.AppendNullableContextDependingOnTarget(/*extensionClass.NullableAnnotation*/ NullableAnnotation.None)

.AppendNullableContext(in options)
.AppendLine()
.AppendLine(string.IsNullOrWhiteSpace(ns) ? "" : $@"namespace {ns}")
.AppendNamespace(in extensionClass)
.AppendLine("{")
.Append(" [global::System.CodeDom.Compiler.GeneratedCode(\"Ossendorf.Csla.DataportalExtensionsGenerator\", \"").Append(GeneratorHelper.VersionString).AppendLine("\")]")
.AppendLine(" [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage(Justification = \"Generated by the Ossendorf.Csla.DataPortalExtensionsGenerators source generator.\")]")
.AppendLine($" static partial class {name}")
.AppendClassDeclaration(in extensionClass)
.AppendLine(" {")
.AppendMethodsGroupedByClass(in methods, in options, ct)
.AppendLine(" }")
Expand Down
90 changes: 1 addition & 89 deletions src/Csla.DataPortalExtensionGenerator/GeneratorHelper.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Immutable;
using System.Text;

namespace Ossendorf.Csla.DataPortalExtensionGenerator;
namespace Ossendorf.Csla.DataPortalExtensionGenerator;
internal static class GeneratorHelper {

public const string FullyQalifiedNameOfMarkerAttribute = "Ossendorf.Csla.DataPortalExtensionGenerator.DataPortalExtensionsAttribute";
Expand All @@ -29,90 +25,6 @@ public class {MarkerAttributeNameWithSuffix} : global::System.Attribute {{
}}
}}";

public static StringBuilder AppendMethodsGroupedByClass(this StringBuilder sb, in ImmutableArray<PortalOperationToGenerate> foundOperations, in GeneratorOptions options, CancellationToken ct) {
const string intendation = " ";

var groupedByClass = foundOperations.Cast<PortalOperationToGenerate>().GroupBy(o => o.Object).ToImmutableArray();

foreach (var operationsByClass in groupedByClass) {
ct.ThrowIfCancellationRequested();

if (!operationsByClass.Any()) {
continue;
}

var boName = operationsByClass.Key.GloballyQualifiedName;

foreach (var operation in operationsByClass) {
ct.ThrowIfCancellationRequested();

var childPrefix = IsChildMethod(operation.PortalMethod) ? "Child" : "";
string returnType;
if (operation.PortalMethod is DataPortalMethod.Delete) {
returnType = "Task";
} else {
returnType = $"Task<{boName}>";
}

var (parameters, arguments) = GetParametersAndArgumentsToUse(operation.Parameters, ct);

var visibilityModifier = operationsByClass.Key.HasPublicModifier && operation.Parameters.All(p => p.IsPublic) ? "public" : "internal";

_ = sb.Append(intendation)
.Append(visibilityModifier).Append(" static ")
.Append("global::System.Threading.Tasks.").Append(returnType).Append(" ").Append(options.MethodPrefix).Append(operation.MethodName).Append(options.MethodSuffix)
.Append("(this global::Csla.I").Append(childPrefix).Append("DataPortal<").Append(boName).Append("> ")
.Append("portal").Append(parameters).Append(")")
.Append(" => portal.").Append(operation.PortalMethod.ToStringFast()).Append("Async")
.Append("(").Append(arguments).Append(");").AppendLine();
}
}

return sb;

static bool IsChildMethod(DataPortalMethod portalMethod) {
return portalMethod switch {
DataPortalMethod.FetchChild or DataPortalMethod.CreateChild => true,
DataPortalMethod.Fetch or DataPortalMethod.Delete or DataPortalMethod.Create or DataPortalMethod.Execute => false,
_ => throw new InvalidOperationException($"Unknown dataportal method {portalMethod}"),
};
}
}

private static (StringBuilder Parameters, StringBuilder Arguments) GetParametersAndArgumentsToUse(EquatableArray<OperationParameter> parameters, CancellationToken ct) {
var parametersBuilder = new StringBuilder();
var argumentsBuilder = new StringBuilder();
if (parameters.Count == 0) {
return (parametersBuilder, argumentsBuilder);
}

foreach (var parameter in parameters) {
ct.ThrowIfCancellationRequested();

if (parametersBuilder.Length > 0) {
parametersBuilder.Append(", ");
argumentsBuilder.Append(", ");
}

parametersBuilder.Append(parameter.ParameterFormatted);
argumentsBuilder.Append(parameter.ArgumentFormatted);
}

if (parametersBuilder.Length > 0) {
parametersBuilder.Insert(0, ", ");
}

return (parametersBuilder, argumentsBuilder);
}

public static string ExtractAttributeName(NameSyntax? name) {
return name switch {
SimpleNameSyntax ins => ins.Identifier.Text,
QualifiedNameSyntax qns => qns.Right.Identifier.Text,
_ => ""
};
}

private static readonly Dictionary<string, DataPortalMethod> _methodTranslations = [];
static GeneratorHelper() {
foreach (var portalMethod in Enum.GetValues(typeof(DataPortalMethod)).Cast<DataPortalMethod>()) {
Expand Down
10 changes: 8 additions & 2 deletions src/Csla.DataPortalExtensionGenerator/GeneratorOptions.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
namespace Ossendorf.Csla.DataPortalExtensionGenerator;
using Microsoft.CodeAnalysis;

namespace Ossendorf.Csla.DataPortalExtensionGenerator;

internal readonly record struct GeneratorOptions {
public readonly string MethodPrefix;
public readonly string MethodSuffix;
public readonly NullableContextOptions NullableContextOptions;
public readonly bool SuppressWarningCS8669;

public GeneratorOptions(string methodPrefix, string methodSuffix) {
public GeneratorOptions(string methodPrefix, string methodSuffix, NullableContextOptions nullableContextOptions, bool suppressWarningCS8669) {
MethodPrefix = methodPrefix;
MethodSuffix = methodSuffix;
NullableContextOptions = nullableContextOptions;
SuppressWarningCS8669 = suppressWarningCS8669;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@
<ItemGroup>
<CompilerVisibleProperty Include="DataPortalExtensionGen_MethodPrefix" />
<CompilerVisibleProperty Include="DataPortalExtensionGen_MethodSuffix" />
<CompilerVisibleProperty Include="DataPortalExtensionGen_NullableContext" />
<CompilerVisibleProperty Include="DataPortalExtensionGen_SuppressWarningCS8669" />
</ItemGroup>
</Project>
Loading

0 comments on commit f5df472

Please sign in to comment.