Skip to content

Commit

Permalink
Add analyzer and code fix for making unions partial
Browse files Browse the repository at this point in the history
=> release
  • Loading branch information
hugener committed Mar 6, 2024
1 parent d4c0107 commit c15da01
Show file tree
Hide file tree
Showing 37 changed files with 369 additions and 112 deletions.
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Either specify the partial keyword to the union for a source generator to implem
### Defining a union
```csharp
[Sundew.DiscriminatedUnions.DiscriminatedUnion]
public abstract record Result
public abstract partial record Result
{
public sealed record Success : Result;

Expand Down Expand Up @@ -118,12 +118,11 @@ In addition, the DiscriminatedUnion attribute can specify a flags enum (Generato
| SDU0008 | Cases should be sealed | yes |
| SDU0009 | Unnested cases should have factory method | PDU0001 |
| SDU0010 | Factory method should have correct CaseTypeAttribute | yes |
| PDU0001 | Populate union factory methods | yes |
| PDU0001 | Make union partial for code generator | yes |
| PDU0002 | Populate union factory methods | yes |
| SDU9999 | Switch should throw in default case | no |
| GDU0001 | Discriminated union declaration could not be found | no |

## Issues/Todos
* Switch appears with red squiggly lines in VS: https://github.com/dotnet/roslyn/issues/57041
* Nullability is falsely evaluated when the switch hints null is possible: https://github.com/dotnet/roslyn/issues/57042
* SDU0009 gets reported in VS, but no code fix is offered. VS issue here: https://github.com/dotnet/roslyn/issues/57621
* Workaround: A PDU0001 is always reported on unions offering to generate factory methods, as it is not technically possible to implement a code fix for SDU0009 (See issue).
* Nullability is falsely evaluated when the switch hints null is possible: https://github.com/dotnet/roslyn/issues/57042
2 changes: 1 addition & 1 deletion Source/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Deterministic>true</Deterministic>
<Version>5.2</Version>
<Version>5.3</Version>
<OutputPath>bin/$(Configuration)</OutputPath>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ namespace Sundew.DiscriminatedUnions.Analyzer;
using Sundew.DiscriminatedUnions.Analyzer.FactoryMethod;
using Sundew.DiscriminatedUnions.Analyzer.SwitchExpression;
using Sundew.DiscriminatedUnions.Analyzer.SwitchStatement;
using Sundew.DiscriminatedUnions.Shared;

/// <summary>
/// Discriminated Union analyzer that ensures all cases in switch statements and expression are handled and that the code defining the discriminated union is a closed inheritance hierarchy.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="MakePartialMarkerAnalyzer.cs" company="Sundews">
// Copyright (c) Sundews. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace Sundew.DiscriminatedUnions.Analyzer;

using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Sundew.DiscriminatedUnions.Shared;

/// <summary>
/// Marks all union types with a diagnostic, so that a code fix can be offered to generate factory methods.
/// </summary>
/// <seealso cref="Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer" />
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class MakePartialMarkerAnalyzer : DiagnosticAnalyzer
{
/// <summary>
/// Diagnostic id for a union partial.
/// </summary>
public const string MakeUnionPartialDiagnosticId = "PDU0001";

/// <summary>
/// The switch should throw in default case rule.
/// </summary>
public static readonly DiagnosticDescriptor MakeUnionPartialRule =
Sundew.DiscriminatedUnions.Analyzer.DiagnosticDescriptorHelper.Create(
MakeUnionPartialDiagnosticId,
nameof(Resources.MakeUnionPartialTitle),
nameof(Resources.MakeUnionPartialMessageFormat),
Category,
DiagnosticSeverity.Info,
true,
nameof(Resources.MakeUnionPartialDescription));

private const string Category = "CodeGeneration";

/// <summary>
/// Gets a set of descriptors for the diagnostics that this analyzer is capable of producing.
/// </summary>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(MakeUnionPartialRule);

/// <summary>
/// Called once at session start to register actions in the analysis context.
/// </summary>
/// <param name="context">The context.</param>
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();

context.RegisterSymbolAction(
symbolAnalysisContext =>
{
if (symbolAnalysisContext.Symbol is not INamedTypeSymbol namedTypeSymbol)
{
return;
}

if (namedTypeSymbol.IsDiscriminatedUnion() &&
(namedTypeSymbol.IsAbstract || namedTypeSymbol.TypeKind == TypeKind.Interface) && !IsPartial(namedTypeSymbol))
{
foreach (var location in namedTypeSymbol.Locations)
{
symbolAnalysisContext.ReportDiagnostic(
Diagnostic.Create(
MakeUnionPartialRule,
location,
DiagnosticSeverity.Info,
null,
null,
namedTypeSymbol));
}
}
},
SymbolKind.NamedType);
}

private static bool IsPartial(INamedTypeSymbol namedTypeSymbol)
{
var syntax = namedTypeSymbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax();
if (syntax is TypeDeclarationSyntax typeDeclarationSyntax)
{
return typeDeclarationSyntax.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
}

return false;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="PopulateUnionFactoryMethodsMarkerAnalyzer.cs" company="Sundews">
// <copyright file="PopulateFactoryMethodsMarkerAnalyzer.cs" company="Sundews">
// Copyright (c) Sundews. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
// </copyright>
Expand All @@ -8,7 +8,10 @@
namespace Sundew.DiscriminatedUnions.Analyzer;

using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Sundew.DiscriminatedUnions.Shared;

Expand All @@ -17,12 +20,12 @@ namespace Sundew.DiscriminatedUnions.Analyzer;
/// </summary>
/// <seealso cref="Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer" />
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class PopulateUnionFactoryMethodsMarkerAnalyzer : DiagnosticAnalyzer
public class PopulateFactoryMethodsMarkerAnalyzer : DiagnosticAnalyzer
{
/// <summary>
/// Diagnostic id for populating factory methods in a union.
/// </summary>
public const string PopulateFactoryMethodsDiagnosticId = "PDU0001";
public const string PopulateFactoryMethodsDiagnosticId = "PDU0002";

/// <summary>
/// The switch should throw in default case rule.
Expand All @@ -33,7 +36,7 @@ public class PopulateUnionFactoryMethodsMarkerAnalyzer : DiagnosticAnalyzer
nameof(Resources.PopulateFactoryMethodsTitle),
nameof(Resources.PopulateFactoryMethodsMessageFormat),
Category,
DiagnosticSeverity.Error,
DiagnosticSeverity.Info,
true,
nameof(Resources.PopulateFactoryMethodsDescription));

Expand Down Expand Up @@ -62,7 +65,7 @@ public override void Initialize(AnalysisContext context)
}

if (namedTypeSymbol.IsDiscriminatedUnion() &&
(namedTypeSymbol.IsAbstract || namedTypeSymbol.TypeKind == TypeKind.Interface))
(namedTypeSymbol.IsAbstract || namedTypeSymbol.TypeKind == TypeKind.Interface) && !IsPartial(namedTypeSymbol))
{
foreach (var location in namedTypeSymbol.Locations)
{
Expand All @@ -79,4 +82,15 @@ public override void Initialize(AnalysisContext context)
},
SymbolKind.NamedType);
}

private static bool IsPartial(INamedTypeSymbol namedTypeSymbol)
{
var syntax = namedTypeSymbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax();
if (syntax is TypeDeclarationSyntax typeDeclarationSyntax)
{
return typeDeclarationSyntax.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
}

return false;
}
}
27 changes: 27 additions & 0 deletions Source/Sundew.DiscriminatedUnions.Analyzer/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions Source/Sundew.DiscriminatedUnions.Analyzer/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -240,4 +240,13 @@
<data name="FactoryMethodShouldHaveMatchingCaseTypeAttributeTitle" xml:space="preserve">
<value>Factory method must have CaseTypeAttribute.</value>
</data>
<data name="MakeUnionPartialDescription" xml:space="preserve">
<value>Makes the union partial for the code generator can create factory methods.</value>
</data>
<data name="MakeUnionPartialMessageFormat" xml:space="preserve">
<value>'{0}' should be partial to enable the code generator.</value>
</data>
<data name="MakeUnionPartialTitle" xml:space="preserve">
<value>Make union partial.</value>
</data>
</root>

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@
<value>Make '{0}' internal</value>
</data>
<data name="InsertCorrectCaseTypeAttribute" xml:space="preserve">
<value>Insert the correct CaseTypeAttribute.</value>
<value>Insert the correct CaseTypeAttribute</value>
</data>
<data name="MakePartial" xml:space="preserve">
<value>Make union '{0}' partial</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public DiscriminatedUnionsCodeFixProvider()
new SwitchHasUnreachableNullCaseCodeFixer(),
new UnionsMustBeAbstractCodeFixer(),
new CasesShouldBeSealedCodeFixer(),
new MakePartialCodeFixer(),
new PopulateFactoryMethodsCodeFixer(),
new CaseTypeAttributeCodeFixer(),
}.ToDictionary(x => x.DiagnosticId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="MakePartialCodeFixer.cs" company="Sundews">
// Copyright (c) Sundews. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace Sundew.DiscriminatedUnions.CodeFixes;

using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Sundew.DiscriminatedUnions.Analyzer;

internal class MakePartialCodeFixer : ICodeFixer
{
public string DiagnosticId => MakePartialMarkerAnalyzer.MakeUnionPartialDiagnosticId;

public CodeFixStatus GetCodeFixState(
SyntaxNode syntaxNode,
SemanticModel semanticModel,
Diagnostic diagnostic,
CancellationToken cancellationToken)
{
var declaredSymbol = semanticModel.GetDeclaredSymbol(syntaxNode, cancellationToken);
if (declaredSymbol == null)
{
return new CodeFixStatus.CannotFix();
}

var name = string.Format(CodeFixResources.MakePartial, declaredSymbol.Name);
return new CodeFixStatus.CanFix(
name,
nameof(MakePartialCodeFixer));
}

public async Task<Document> Fix(
Document document,
SyntaxNode root,
SyntaxNode node,
IReadOnlyList<Location> additionalLocations,
ImmutableDictionary<string, string?> diagnosticProperties,
SemanticModel semanticModel,
CancellationToken cancellationToken)
{
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
var generator = editor.Generator;
var declaration = generator.WithModifiers(node, generator.GetModifiers(node).WithPartial(true)).WithAdditionalAnnotations(Formatter.Annotation);
var newNode = root.ReplaceNode(node, declaration);
return document.WithSyntaxRoot(newNode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ namespace Sundew.DiscriminatedUnions.CodeFixes;

internal class PopulateFactoryMethodsCodeFixer : ICodeFixer
{
public string DiagnosticId => PopulateUnionFactoryMethodsMarkerAnalyzer.PopulateFactoryMethodsDiagnosticId;
public string DiagnosticId => PopulateFactoryMethodsMarkerAnalyzer.PopulateFactoryMethodsDiagnosticId;

public CodeFixStatus GetCodeFixState(
SyntaxNode syntaxNode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Sundew.Base.Collections
/// Segregation extension method for AllOrFailed.
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCode]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Sundew.DiscriminateUnions.Generator", "5.2.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Sundew.DiscriminateUnions.Generator", "5.3.0.0")]
public static partial class AllOrFailedExtensions
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Sundew.Base.Collections
/// Contains individual lists of the different cases of the discriminated union AllOrFailed.
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCode]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Sundew.DiscriminateUnions.Generator", "5.2.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Sundew.DiscriminateUnions.Generator", "5.3.0.0")]
public sealed partial class AllOrFailedSegregation<TItem, TResult, TError>
where TItem : class, global::System.IEquatable<TItem>
where TResult : struct
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Sundew.Base.Collections
{
#pragma warning disable SA1601
[global::System.Diagnostics.DebuggerNonUserCode]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Sundew.DiscriminateUnions.Generator", "5.2.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Sundew.DiscriminateUnions.Generator", "5.3.0.0")]
public partial class AllOrFailed<TItem, TResult, TError>
where TItem : class, global::System.IEquatable<TItem>
where TResult : struct
Expand Down
Loading

0 comments on commit c15da01

Please sign in to comment.