Skip to content

Commit

Permalink
- remove base class name from generated parameter names and static fa…
Browse files Browse the repository at this point in the history
…ctory method names. See docs about static factory methods.

- version of generators package to 4.0, because generated Match parameter and static method names may change
  • Loading branch information
ax0l0tl committed Jun 14, 2024
1 parent 64aa095 commit 35fa653
Show file tree
Hide file tree
Showing 35 changed files with 1,762 additions and 304 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,26 @@ Those factory methods are not generated if they would conflict with an existing
So you can always decide to implement them by yourself. Generation of factory methods on a partial base type can be suppressed
by setting StaticFactoryMethods argument to false: `[UnionType(StaticFactoryMethods=false)]`. Currently default values in
constructor parameters from namespaces other than System need full qualification.
If you like to declare your cases as nested types of your base types you can use an underscore prefix or postfix with your nested type name to avoid conflicts with factory methods. Static factory method will then be generated without the underscore. This also works if you use the base type name as prefix or postfix.

``` cs
[UnionType]
public abstract partial record Failure
{
public record NotFound_(int Id) : Failure;
}

public record InvalidInputFailure(string Message) : Failure;

class ExampleConsumer
{
public static void UseGeneratedFactoryMethods()
{
var notFound = Failure.NotFound(42); //static factory method generated without underscore used in typename NotFound_
var invalid = Failure.InvalidInput("I don't like it"); //static factory method generated without base typename postfix
}
}
```

If you like union types but don't like excessive typing in C# try the [Switchyard](https://github.com/bluehands/Switchyard) Visual Studio extension, which generates the boilerplate code for you. It plays nicely with the FunicularSwitch.Generators package.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
</VersionSuffixLocal>

<!--#region adapt versions here-->
<MajorVersion>3</MajorVersion>
<MinorAndPatchVersion>3.2</MinorAndPatchVersion>
<MajorVersion>4</MajorVersion>
<MinorAndPatchVersion>0.0</MinorAndPatchVersion>
<!--#endregion-->

<AssemblyVersion>$(MajorVersion).0.0</AssemblyVersion>
Expand Down
16 changes: 15 additions & 1 deletion Source/FunicularSwitch.Generators/Generation/Indent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,21 @@ public static string ToParameterName(this string name)
{
var typeNameWithoutOuter = name.Split('.').Last();
var parameterName = typeNameWithoutOuter.FirstToLower();
return parameterName.IsAnyKeyWord() ? $"@{parameterName}" : parameterName;
return PrefixAtIfKeyword(parameterName);
}

public static string PrefixAtIfKeyword(this string parameterName) => parameterName.IsAnyKeyWord() ? $"@{parameterName}" : parameterName;

public static string TrimBaseTypeName(this string value, string baseTypeName)
{
if (value.Length <= baseTypeName.Length)
return value;

if (value.EndsWith(baseTypeName))
value = value.Substring(0, value.Length - baseTypeName.Length);
else if (value.StartsWith(baseTypeName))
value = value.Substring(baseTypeName.Length);
return value;
}

public static string ToMatchExtensionFilename(this string fullTypeName) => $"{fullTypeName.Replace(".", "")}MatchExtension.g.cs";
Expand Down
15 changes: 9 additions & 6 deletions Source/FunicularSwitch.Generators/UnionType/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,15 @@ static void WritePartialWithStaticFactories(UnionTypeSchema unionTypeSchema, CSh
{
var info = unionTypeSchema.StaticFactoryInfo!;

builder.WriteLine($"{(info.Modifiers.Select(m => m.Text).ToSeparatedString(" "))} {(unionTypeSchema.IsRecord ? "record" : "class")} {unionTypeSchema.TypeName}");
var typeKind = unionTypeSchema.TypeKind switch { UnionTypeTypeKind.Class => "class", UnionTypeTypeKind.Interface => "interface", UnionTypeTypeKind.Record => "record", _ => throw new ArgumentException($"Unknown type kind: {unionTypeSchema.TypeKind}") };
builder.WriteLine($"{(info.Modifiers.Select(m => m.Text).ToSeparatedString(" "))} {typeKind} {unionTypeSchema.TypeName}");
using (builder.Scope())
{
foreach (var derivedType in unionTypeSchema.Cases)
{
var nameParts = derivedType.FullTypeName.Split('.');
var derivedTypeName = nameParts[nameParts.Length - 1];
var methodName = derivedTypeName.Trim('_');
var methodName = derivedType.StaticFactoryMethodName;

if ($"{unionTypeSchema.FullTypeName}.{methodName}" == derivedType.FullTypeName) //union case is nested type without underscores, so factory method name would conflict with type name
continue;
Expand Down Expand Up @@ -127,7 +128,7 @@ static void WritePartialWithStaticFactories(UnionTypeSchema unionTypeSchema, CSh
}
}

static void GenerateMatchMethod(CSharpBuilder builder, UnionTypeSchema unionTypeSchema, string t)
static void GenerateMatchMethod(CSharpBuilder builder, UnionTypeSchema unionTypeSchema, string t)
{
var thisParameterType = unionTypeSchema.FullTypeName;
var thisParameter = ThisParameter(unionTypeSchema, thisParameterType);
Expand All @@ -140,7 +141,8 @@ static void GenerateMatchMethod(CSharpBuilder builder, UnionTypeSchema unionType
foreach (var c in unionTypeSchema.Cases)
{
caseIndex++;
builder.WriteLine($"{c.FullTypeName} case{caseIndex} => {c.ParameterName}(case{caseIndex}),");
var caseVariableName = $"{c.ParameterName}{caseIndex}";
builder.WriteLine($"{c.FullTypeName} {caseVariableName} => {c.ParameterName}({caseVariableName}),");
}

builder.WriteLine(
Expand All @@ -163,10 +165,11 @@ static void GenerateSwitchMethod(CSharpBuilder builder, UnionTypeSchema unionTyp
foreach (var c in unionTypeSchema.Cases)
{
caseIndex++;
builder.WriteLine($"case {c.FullTypeName} case{caseIndex}:");
var caseVariableName = $"{c.ParameterName}{caseIndex}";
builder.WriteLine($"case {c.FullTypeName} {caseVariableName}:");
using (builder.Indent())
{
var call = $"{c.ParameterName}(case{caseIndex})";
var call = $"{c.ParameterName}({caseVariableName})";
if (isAsync)
call = $"await {call}.ConfigureAwait(false)";
builder.WriteLine($"{call};");
Expand Down
58 changes: 49 additions & 9 deletions Source/FunicularSwitch.Generators/UnionType/Parser.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Immutable;
using FunicularSwitch.Generators.Common;
using FunicularSwitch.Generators.Generation;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand Down Expand Up @@ -47,24 +48,56 @@ public static IEnumerable<UnionTypeSchema> GetUnionTypes(Compilation compilation
});

var isPartial = unionTypeClass.Modifiers.HasModifier(SyntaxKind.PartialKeyword);
var generateFactoryMethods = isPartial && unionTypeClass is not InterfaceDeclarationSyntax &&
var generateFactoryMethods = isPartial /*&& unionTypeClass is not InterfaceDeclarationSyntax*/ &&
staticFactoryMethods;
return new UnionTypeSchema(
var cases =
ToOrderedCases(caseOrder, derivedTypes, reportDiagnostic, compilation, generateFactoryMethods, unionTypeSymbol.Name)
.ToImmutableArray();

return new UnionTypeSchema(
Namespace: fullNamespace,
TypeName: unionTypeSymbol.Name,
FullTypeName: fullTypeName,
Cases: ToOrderedCases(caseOrder, derivedTypes, reportDiagnostic, compilation, generateFactoryMethods)
.ToImmutableArray(),
Cases: cases,
IsInternal: acc is Accessibility.NotApplicable or Accessibility.Internal,
IsPartial: isPartial,
IsRecord: unionTypeClass is RecordDeclarationSyntax,
StaticFactoryInfo: generateFactoryMethods
TypeKind: unionTypeClass switch
{
RecordDeclarationSyntax => UnionTypeTypeKind.Record,
InterfaceDeclarationSyntax => UnionTypeTypeKind.Interface,
_ => UnionTypeTypeKind.Class
},
StaticFactoryInfo: generateFactoryMethods
? BuildFactoryInfo(unionTypeClass, compilation)
: null
);
})
.Where(unionTypeClass => unionTypeClass is { Cases.Count: > 0 });

static (string parameterName, string methodName) DeriveParameterAndStaticMethodName(string typeName,
string baseTypeName)
{
var candidates = ImmutableList<string>.Empty;
candidates = AddIfDiffersAndValid(typeName.TrimBaseTypeName(baseTypeName));
candidates = AddIfDiffersAndValid(typeName.Trim('_'));
candidates = candidates.Add(typeName);

var parameterName = candidates[0].FirstToLower().PrefixAtIfKeyword();
var methodName = candidates[0].FirstToUpper().PrefixAtIfKeyword();

return (parameterName, methodName);

ImmutableList<string> AddIfDiffersAndValid(string candidate) =>
DiffersAndValid(typeName, candidate)
? candidates.Add(candidate)
: candidates;
}

static bool DiffersAndValid(string typeName, string candidate) =>
candidate != typeName
&& !string.IsNullOrEmpty(candidate)
&& char.IsLetter(candidate[0]);

static StaticFactoryMethodsInfo BuildFactoryInfo(BaseTypeDeclarationSyntax unionTypeClass, Compilation compilation)
{
var staticMethods = unionTypeClass.ChildNodes()
Expand Down Expand Up @@ -114,7 +147,10 @@ PropertyDeclarationSyntax p when p.Modifiers.HasModifier(SyntaxKind.StaticKeywor
return (caseOrder, staticFactoryMethods);
}

static IEnumerable<DerivedType> ToOrderedCases(CaseOrder caseOrder, IEnumerable<(INamedTypeSymbol symbol, BaseTypeDeclarationSyntax node, int? caseIndex, int numberOfConctreteBaseTypes)> derivedTypes, Action<Diagnostic> reportDiagnostic, Compilation compilation, bool getConstructors)
static IEnumerable<DerivedType> ToOrderedCases(CaseOrder caseOrder,
IEnumerable<(INamedTypeSymbol symbol, BaseTypeDeclarationSyntax node, int? caseIndex, int
numberOfConctreteBaseTypes)> derivedTypes, Action<Diagnostic> reportDiagnostic, Compilation compilation,
bool getConstructors, string baseTypeName)
{
var ordered = derivedTypes.OrderByDescending(d => d.numberOfConctreteBaseTypes);
ordered = caseOrder switch
Expand Down Expand Up @@ -176,10 +212,14 @@ static IEnumerable<DerivedType> ToOrderedCases(CaseOrder caseOrder, IEnumerable<
});
}

var (parameterName, staticMethodName) =
DeriveParameterAndStaticMethodName(qualifiedTypeName.Name, baseTypeName);

return new DerivedType(
fullTypeName: $"{(fullNamespace != null ? $"{fullNamespace}." : "")}{qualifiedTypeName}",
typeName: qualifiedTypeName.Name,
constructors: constructors?.ToImmutableList());
constructors: constructors?.ToImmutableList(),
parameterName: parameterName,
staticFactoryMethodName: staticMethodName);
});
}
}
Expand Down
22 changes: 15 additions & 7 deletions Source/FunicularSwitch.Generators/UnionType/UnionTypeSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,16 @@ public sealed record UnionTypeSchema(string? Namespace,
IReadOnlyCollection<DerivedType> Cases,
bool IsInternal,
bool IsPartial,
bool IsRecord,
UnionTypeTypeKind TypeKind,
StaticFactoryMethodsInfo? StaticFactoryInfo);

public enum UnionTypeTypeKind
{
Class,
Record,
Interface
}

public record StaticFactoryMethodsInfo(
IReadOnlyCollection<MemberInfo> ExistingStaticMethods,
IReadOnlyCollection<string> ExistingStaticFields,
Expand All @@ -23,14 +30,15 @@ SyntaxTokenList Modifiers
public sealed record DerivedType
{
public string FullTypeName { get; }
public IReadOnlyCollection<MemberInfo> Constructors { get; }
public string ParameterName { get; }

public IReadOnlyCollection<MemberInfo> Constructors { get; }

public DerivedType(string fullTypeName, string typeName, IReadOnlyCollection<MemberInfo>? constructors = null)
public string StaticFactoryMethodName { get; }

public DerivedType(string fullTypeName, string parameterName, string staticFactoryMethodName, IReadOnlyCollection<MemberInfo>? constructors = null)
{
FullTypeName = fullTypeName;
Constructors = constructors ?? ImmutableList<MemberInfo>.Empty;
ParameterName = (typeName.Any(c => c != '_') ? typeName.TrimEnd('_') : typeName).ToParameterName();
ParameterName = parameterName;
StaticFactoryMethodName = staticFactoryMethodName;
Constructors = constructors ?? [];
}
}
9 changes: 8 additions & 1 deletion Source/FunicularSwitch.sln
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "FunicularSwitch.Generators.
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "FunicularSwitch.Common", "FunicularSwitch.Common\FunicularSwitch.Common.shproj", "{BF664635-17E2-44EB-A99E-94D564542509}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FunicularSwitch.Generators.FluentAssertions.Consumer.Dependency", "Tests\FunicularSwitch.Generators.FluentAssertions.Consumer.Dependency\FunicularSwitch.Generators.FluentAssertions.Consumer.Dependency.csproj", "{DA09911F-FDE5-477E-BEEA-76D582844408}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FunicularSwitch.Generators.FluentAssertions.Consumer.Dependency", "Tests\FunicularSwitch.Generators.FluentAssertions.Consumer.Dependency\FunicularSwitch.Generators.FluentAssertions.Consumer.Dependency.csproj", "{DA09911F-FDE5-477E-BEEA-76D582844408}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FunicularSwitch.Generators.Consumer.StandardMinLangVersion", "Tests\FunicularSwitch.Generators.Consumer.StandardMinLangVersion\FunicularSwitch.Generators.Consumer.StandardMinLangVersion.csproj", "{18D4F137-98AF-47CF-88FE-313C4BEA4215}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -104,6 +106,10 @@ Global
{DA09911F-FDE5-477E-BEEA-76D582844408}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DA09911F-FDE5-477E-BEEA-76D582844408}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DA09911F-FDE5-477E-BEEA-76D582844408}.Release|Any CPU.Build.0 = Release|Any CPU
{18D4F137-98AF-47CF-88FE-313C4BEA4215}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{18D4F137-98AF-47CF-88FE-313C4BEA4215}.Debug|Any CPU.Build.0 = Debug|Any CPU
{18D4F137-98AF-47CF-88FE-313C4BEA4215}.Release|Any CPU.ActiveCfg = Release|Any CPU
{18D4F137-98AF-47CF-88FE-313C4BEA4215}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -116,6 +122,7 @@ Global
{DE65F0E3-12EA-48AA-98A3-DB7D636C28D0} = {3411C93C-2928-49DF-8180-90DDB1F8FB6C}
{672EACCA-A632-4D85-B36F-C1C83638C222} = {3411C93C-2928-49DF-8180-90DDB1F8FB6C}
{DA09911F-FDE5-477E-BEEA-76D582844408} = {3411C93C-2928-49DF-8180-90DDB1F8FB6C}
{18D4F137-98AF-47CF-88FE-313C4BEA4215} = {3411C93C-2928-49DF-8180-90DDB1F8FB6C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0A6C53D4-8DAD-4DBF-B4A6-D02801D28BD4}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net4.8</TargetFramework>
<LangVersion>9.0</LangVersion>
</PropertyGroup>

<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
<RootNamespace>StandardMinLangVersion</RootNamespace>
</PropertyGroup>

<ItemGroup>
<!--Don't include the output from a previous source generator execution into future runs; the */** trick here ensures that there's
at least one subdirectory, which is our key that it's coming from a source generator as opposed to something that is coming from
some other tool.-->
<Compile Remove="$(CompilerGeneratedFilesOutputPath)/*/**/*.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="MSTest.TestAdapter" Version="3.4.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\FunicularSwitch.Generators\FunicularSwitch.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System;

// ReSharper disable once CheckNamespace
namespace FunicularSwitch.Generators
{
[AttributeUsage(AttributeTargets.Enum)]
sealed class ExtendedEnumAttribute : Attribute
{
public EnumCaseOrder CaseOrder { get; set; } = EnumCaseOrder.AsDeclared;
public ExtensionAccessibility Accessibility { get; set; } = ExtensionAccessibility.Public;
}

enum EnumCaseOrder
{
Alphabetic,
AsDeclared
}

/// <summary>
/// Generate match methods for all enums defined in assembly that contains AssemblySpecifier.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
class ExtendEnumsAttribute : Attribute
{
public Type AssemblySpecifier { get; }
public EnumCaseOrder CaseOrder { get; set; } = EnumCaseOrder.AsDeclared;
public ExtensionAccessibility Accessibility { get; set; } = ExtensionAccessibility.Public;

public ExtendEnumsAttribute() => AssemblySpecifier = typeof(ExtendEnumsAttribute);

public ExtendEnumsAttribute(Type assemblySpecifier)
{
AssemblySpecifier = assemblySpecifier;
}
}

/// <summary>
/// Generate match methods for Type. Must be enum.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
class ExtendEnumAttribute : Attribute
{
public Type Type { get; }

public EnumCaseOrder CaseOrder { get; set; } = EnumCaseOrder.AsDeclared;

public ExtensionAccessibility Accessibility { get; set; } = ExtensionAccessibility.Public;

public ExtendEnumAttribute(Type type)
{
Type = type;
}
}

enum ExtensionAccessibility
{
Internal,
Public
}
}
Loading

0 comments on commit 35fa653

Please sign in to comment.