-
-
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.
Create analyzer and codefix for templates
Templates need to adhere to some minimal rules: 1. File-local visibility and partial 2. Record struct themselves (since they will be partial of another record struct) 3. Optional primary ctor with Value parameter to allow template code to operate on the underlying value of the struct id as needed. We now enforce these rules via an analyzer and provide a codefix to help authoring.
- Loading branch information
Showing
8 changed files
with
595 additions
and
8 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
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,61 @@ | ||
using System.Collections.Immutable; | ||
using System.Diagnostics; | ||
using System.Linq; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Microsoft.CodeAnalysis.Diagnostics; | ||
using static StructId.Diagnostics; | ||
|
||
namespace StructId; | ||
|
||
[DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
public class TemplateAnalyzer : DiagnosticAnalyzer | ||
{ | ||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics | ||
=> ImmutableArray.Create(TemplateMustBeFileRecordStruct, TemplateConstructorValueConstructor, TemplateDeclarationNotTSelf); | ||
|
||
public override void Initialize(AnalysisContext context) | ||
{ | ||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||
|
||
if (!Debugger.IsAttached) | ||
context.EnableConcurrentExecution(); | ||
|
||
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.ClassDeclaration); | ||
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.StructDeclaration); | ||
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.RecordDeclaration); | ||
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.RecordStructDeclaration); | ||
} | ||
|
||
static void Analyze(SyntaxNodeAnalysisContext context) | ||
{ | ||
var ns = context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.GetStructIdNamespace(); | ||
|
||
if (context.Node is not TypeDeclarationSyntax typeDeclaration || | ||
!typeDeclaration.AttributeLists.Any(list => list.Attributes.Any(attr => attr.IsStructIdTemplate()))) | ||
return; | ||
|
||
var symbol = context.SemanticModel.GetDeclaredSymbol(typeDeclaration); | ||
if (symbol is null) | ||
return; | ||
|
||
if (!symbol.IsFileLocal || !symbol.IsPartial() || !typeDeclaration.IsKind(SyntaxKind.RecordStructDeclaration)) | ||
{ | ||
context.ReportDiagnostic(Diagnostic.Create(TemplateMustBeFileRecordStruct, typeDeclaration.Identifier.GetLocation(), symbol.Name)); | ||
} | ||
|
||
// If there are parameters, it must be only one, and be named Value | ||
if (typeDeclaration.ParameterList is { } parameters) | ||
{ | ||
|
||
if (typeDeclaration.ParameterList.Parameters.Count != 1) | ||
context.ReportDiagnostic(Diagnostic.Create(TemplateConstructorValueConstructor, typeDeclaration.ParameterList.GetLocation(), symbol.Name)); | ||
else if (typeDeclaration.ParameterList.Parameters[0].Identifier.Text != "Value") | ||
context.ReportDiagnostic(Diagnostic.Create(TemplateConstructorValueConstructor, typeDeclaration.ParameterList.Parameters[0].Identifier.GetLocation(), symbol.Name)); | ||
} | ||
|
||
if (typeDeclaration.Identifier.Text != "TSelf") | ||
context.ReportDiagnostic(Diagnostic.Create(TemplateDeclarationNotTSelf, typeDeclaration.Identifier.GetLocation(), symbol.Name)); | ||
} | ||
} |
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,93 @@ | ||
using System.Collections.Immutable; | ||
using System.Composition; | ||
using System.Linq; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CodeActions; | ||
using Microsoft.CodeAnalysis.CodeFixes; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; | ||
using static StructId.Diagnostics; | ||
|
||
namespace StructId; | ||
|
||
[Shared] | ||
[ExportCodeFixProvider(LanguageNames.CSharp)] | ||
public class TemplateCodeFix : CodeFixProvider | ||
{ | ||
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create( | ||
TemplateMustBeFileRecordStruct.Id, TemplateDeclarationNotTSelf.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; | ||
|
||
context.RegisterCodeFix( | ||
new FixerAction(context.Document, root, declaration), | ||
context.Diagnostics); | ||
} | ||
|
||
public class FixerAction(Document document, SyntaxNode root, TypeDeclarationSyntax original) : CodeAction | ||
{ | ||
public override string Title => "Change to file-local partial record struct"; | ||
public override string EquivalenceKey => Title; | ||
|
||
protected override Task<Document> GetChangedDocumentAsync(CancellationToken cancellationToken) | ||
{ | ||
var declaration = original; | ||
var modifiers = declaration.Modifiers; | ||
|
||
if (!modifiers.Any(SyntaxKind.FileKeyword)) | ||
modifiers = modifiers.Insert(0, Token(SyntaxKind.FileKeyword)); | ||
|
||
if (!modifiers.Any(SyntaxKind.PartialKeyword)) | ||
modifiers = modifiers.Insert(1, Token(SyntaxKind.PartialKeyword)); | ||
|
||
// Remove accessibility modifiers which are replaced by 'file' visibility | ||
if (modifiers.FirstOrDefault(x => x.IsKind(SyntaxKind.PublicKeyword)) is { } @public) | ||
modifiers = modifiers.Remove(@public); | ||
if (modifiers.FirstOrDefault(x => x.IsKind(SyntaxKind.InternalKeyword)) is { } @internal) | ||
modifiers = modifiers.Remove(@internal); | ||
if (modifiers.FirstOrDefault(x => x.IsKind(SyntaxKind.PrivateKeyword)) is { } @private) | ||
modifiers = modifiers.Remove(@private); | ||
|
||
if (declaration.Identifier.Text != "TSelf") | ||
declaration = declaration.WithIdentifier(Identifier("TSelf")); | ||
|
||
if (!declaration.IsKind(SyntaxKind.RecordStructDeclaration)) | ||
{ | ||
declaration = RecordDeclaration( | ||
SyntaxKind.RecordStructDeclaration, | ||
declaration.AttributeLists, | ||
modifiers, | ||
Token(SyntaxKind.RecordKeyword), | ||
Token(SyntaxKind.StructKeyword), | ||
declaration.Identifier, | ||
declaration.TypeParameterList, | ||
declaration.ParameterList, | ||
declaration.BaseList, | ||
declaration.ConstraintClauses, | ||
declaration.OpenBraceToken, | ||
declaration.Members, | ||
declaration.CloseBraceToken, | ||
declaration.SemicolonToken); | ||
} | ||
else if (modifiers != declaration.Modifiers) | ||
{ | ||
declaration = declaration.WithModifiers(modifiers); | ||
} | ||
|
||
return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(original, declaration))); | ||
} | ||
} | ||
} |
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
Oops, something went wrong.