-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactory ctor and implicit conversions as template-based
This simplifies mantainability down the road. This also allowed conditional generation of the primary constructor if users already provide one with the required Value parameter. A pair of codefixes are provided to either remove a non-compliant ctor entirely or renaming the parameter to `Value` as required.
- Loading branch information
Showing
14 changed files
with
274 additions
and
55 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,54 +1,17 @@ | ||
using System.Linq; | ||
using System.Text; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
|
||
namespace StructId; | ||
|
||
[Generator(LanguageNames.CSharp)] | ||
public class ConstructorGenerator : IIncrementalGenerator | ||
public class ConstructorGenerator() : TemplateGenerator( | ||
"System.Object", | ||
ThisAssembly.Resources.Templates.Constructor.Text, | ||
ThisAssembly.Resources.Templates.ConstructorT.Text, | ||
ReferenceCheck.TypeExists) | ||
{ | ||
public void Initialize(IncrementalGeneratorInitializationContext context) | ||
{ | ||
var ids = context.CompilationProvider | ||
.SelectMany((x, _) => x.Assembly.GetAllTypes().OfType<INamedTypeSymbol>()) | ||
.Where(t => t.IsStructId()) | ||
.Where(t => t.IsPartial()); | ||
|
||
context.RegisterSourceOutput(ids, GenerateCode); | ||
} | ||
|
||
void GenerateCode(SourceProductionContext context, INamedTypeSymbol symbol) | ||
{ | ||
var ns = symbol.ContainingNamespace.Equals(symbol.ContainingModule.GlobalNamespace, SymbolEqualityComparer.Default) | ||
? null | ||
: symbol.ContainingNamespace.ToDisplayString(); | ||
|
||
// Generic IStructId<T> -> T, otherwise string | ||
var type = symbol.AllInterfaces.First(x => x.Name == "IStructId").TypeArguments.Select(x => x.GetTypeName(ns)).FirstOrDefault() ?? "string"; | ||
|
||
var kind = symbol.IsRecord && symbol.IsValueType ? | ||
"record struct" : | ||
symbol.IsRecord ? | ||
"record" : | ||
"class"; | ||
|
||
var output = new StringBuilder(); | ||
|
||
output.AppendLine("// <auto-generated/>"); | ||
if (ns != null) | ||
output.AppendLine($"namespace {ns};"); | ||
|
||
output.AppendLine( | ||
$$""" | ||
|
||
[System.CodeDom.Compiler.GeneratedCode("StructId", "{{ThisAssembly.Info.InformationalVersion}}")] | ||
partial {{kind}} {{symbol.Name}}({{type}} Value) | ||
{ | ||
public static implicit operator {{type}}({{symbol.Name}} id) => id.Value; | ||
public static explicit operator {{symbol.Name}}({{type}} value) => new(value); | ||
} | ||
"""); | ||
|
||
context.AddSource($"{symbol.ToFileName()}.cs", output.ToString()); | ||
} | ||
} | ||
protected override IncrementalValuesProvider<TemplateArgs> OnInitialize(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider<TemplateArgs> source) | ||
=> base.OnInitialize(context, source.Where(x | ||
=> x.StructId.DeclaringSyntaxReferences.Select(r => r.GetSyntax()).OfType<TypeDeclarationSyntax>().All(s => s.ParameterList == null))); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
using Microsoft.CodeAnalysis; | ||
|
||
namespace StructId; | ||
|
||
[Generator(LanguageNames.CSharp)] | ||
public class ConversionGenerator() : TemplateGenerator( | ||
"System.Object", | ||
ThisAssembly.Resources.Templates.Conversion.Text, | ||
ThisAssembly.Resources.Templates.ConversionT.Text, | ||
ReferenceCheck.TypeExists); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
using System.Collections.Immutable; | ||
using System.Composition; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CodeActions; | ||
using Microsoft.CodeAnalysis.CodeFixes; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; | ||
|
||
namespace StructId; | ||
|
||
[Shared] | ||
[ExportCodeFixProvider(LanguageNames.CSharp)] | ||
public class RemoveCtorCodeFix : CodeFixProvider | ||
{ | ||
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(Diagnostics.MustHaveValueConstructor.Id); | ||
|
||
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; | ||
|
||
public override async Task RegisterCodeFixesAsync(CodeFixContext context) | ||
{ | ||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); | ||
if (root == null) | ||
return; | ||
|
||
var declaration = root.FindNode(context.Span).FirstAncestorOrSelf<TypeDeclarationSyntax>(); | ||
if (declaration == null) | ||
return; | ||
|
||
if (declaration.ParameterList?.Parameters.Count == 1) | ||
context.RegisterCodeFix( | ||
new RemoveAction(context.Document, root, declaration), | ||
context.Diagnostics); | ||
} | ||
|
||
public class RemoveAction(Document document, SyntaxNode root, TypeDeclarationSyntax declaration) : CodeAction | ||
{ | ||
public override string Title => "Remove primary constructor to generate it automatically"; | ||
public override string EquivalenceKey => Title; | ||
|
||
protected override Task<Document> GetChangedDocumentAsync(CancellationToken cancellationToken) | ||
{ | ||
return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(declaration, | ||
declaration.WithParameterList(null)))); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
using System.Collections.Immutable; | ||
using System.Composition; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CodeActions; | ||
using Microsoft.CodeAnalysis.CodeFixes; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; | ||
|
||
namespace StructId; | ||
|
||
[Shared] | ||
[ExportCodeFixProvider(LanguageNames.CSharp)] | ||
public class RenameCtorCodeFix : CodeFixProvider | ||
{ | ||
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(Diagnostics.MustHaveValueConstructor.Id); | ||
|
||
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; | ||
|
||
public override async Task RegisterCodeFixesAsync(CodeFixContext context) | ||
{ | ||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); | ||
if (root == null) | ||
return; | ||
|
||
var parameter = root.FindNode(context.Span).FirstAncestorOrSelf<ParameterSyntax>(); | ||
if (parameter == null) | ||
return; | ||
|
||
context.RegisterCodeFix( | ||
new RenameAction(context.Document, root, parameter), | ||
context.Diagnostics); | ||
} | ||
|
||
public class RenameAction(Document document, SyntaxNode root, ParameterSyntax parameter) : CodeAction | ||
{ | ||
public override string Title => "Rename to 'Value' as required for struct ids"; | ||
public override string EquivalenceKey => Title; | ||
|
||
protected override Task<Document> GetChangedDocumentAsync(CancellationToken cancellationToken) | ||
{ | ||
return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(parameter, | ||
parameter.WithIdentifier(Identifier("Value"))))); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp.Testing; | ||
using Microsoft.CodeAnalysis.Testing; | ||
using Verifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<StructId.RecordAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>; | ||
|
||
namespace StructId; | ||
|
||
public class RecordCtorCodeFixTests | ||
{ | ||
[Fact] | ||
public async Task RenameValue() | ||
{ | ||
var test = new CSharpCodeFixTest<RecordAnalyzer, RenameCtorCodeFix, DefaultVerifier> | ||
{ | ||
ReferenceAssemblies = ReferenceAssemblies.Net.Net80, | ||
TestCode = | ||
""" | ||
using StructId; | ||
|
||
public readonly partial record struct UserId(int {|#0:foo|}) : {|#1:IStructId<int>|}; | ||
""", | ||
FixedCode = | ||
""" | ||
using StructId; | ||
|
||
public readonly partial record struct UserId(int Value) : IStructId<int>; | ||
""", | ||
}.WithCodeFixStructId(); | ||
|
||
test.ExpectedDiagnostics.Add(new DiagnosticResult(Diagnostics.MustHaveValueConstructor).WithLocation(0).WithArguments("UserId")); | ||
test.ExpectedDiagnostics.Add(new DiagnosticResult("CS0535", DiagnosticSeverity.Error).WithLocation(1)); | ||
|
||
// Don't propagate the expected diagnostics to the fixed code, it will have none of them | ||
test.FixedState.InheritanceMode = StateInheritanceMode.Explicit; | ||
|
||
await test.RunAsync(); | ||
} | ||
|
||
[Fact] | ||
public async Task RemoveCtor() | ||
{ | ||
var test = new CSharpCodeFixTest<RecordAnalyzer, RemoveCtorCodeFix, DefaultVerifier> | ||
{ | ||
ReferenceAssemblies = ReferenceAssemblies.Net.Net80, | ||
TestCode = | ||
""" | ||
using StructId; | ||
|
||
public readonly partial record struct UserId(int {|#0:foo|}) : {|#1:IStructId<int>|}; | ||
""", | ||
FixedCode = | ||
""" | ||
using StructId; | ||
|
||
public readonly partial record struct UserId: {|#0:IStructId<int>|}; | ||
""", | ||
}.WithCodeFixStructId(); | ||
|
||
test.ExpectedDiagnostics.Add(new DiagnosticResult(Diagnostics.MustHaveValueConstructor).WithLocation(0).WithArguments("UserId")); | ||
test.ExpectedDiagnostics.Add(new DiagnosticResult("CS0535", DiagnosticSeverity.Error).WithLocation(1)); | ||
|
||
// Don't propagate the expected diagnostics to the fixed code, it will have none of them | ||
test.FixedState.InheritanceMode = StateInheritanceMode.Explicit; | ||
test.FixedState.ExpectedDiagnostics.Add(new DiagnosticResult("CS0535", DiagnosticSeverity.Error).WithLocation(0)); | ||
|
||
await test.RunAsync(); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
// <auto-generated /> | ||
|
||
readonly partial record struct Self(string Value); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
// <auto-generated /> | ||
|
||
readonly partial record struct TSelf(TId Value); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
// <auto-generated /> | ||
|
||
readonly partial record struct Self | ||
{ | ||
public static implicit operator string(Self id) => id.Value; | ||
public static explicit operator Self(string value) => new(value); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
// <auto-generated /> | ||
|
||
readonly partial record struct TSelf | ||
{ | ||
public static implicit operator TId(TSelf id) => id.Value; | ||
public static explicit operator TSelf(TId value) => new(value); | ||
} |
Oops, something went wrong.