-
Notifications
You must be signed in to change notification settings - Fork 3
Refactor SetExplicitMockBehaviorAnalyzer to use KnownSymbols and add CodeFix #296
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 15 commits
deedc38
77aebbe
80c7984
eb55ed2
9908ccb
7d960d5
7c231b9
5872647
594a555
2f095ca
95305de
2ba227e
fdcd576
64001ab
a407efd
858be62
2af5b47
d7c103c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| using System.Diagnostics.CodeAnalysis; | ||
MattKotsenas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| using Microsoft.CodeAnalysis.CodeFixes; | ||
|
|
||
| namespace Moq.CodeFixes; | ||
|
|
||
| internal static class CodeFixContextExtensions | ||
| { | ||
| public static bool TryGetEditProperties(this CodeFixContext context, [NotNullWhen(true)] out DiagnosticEditProperties? editProperties) | ||
MattKotsenas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| ImmutableDictionary<string, string?> properties = context.Diagnostics[0].Properties; | ||
MattKotsenas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Try parsing; if anything fails return false | ||
| editProperties = null; | ||
| if (!properties.TryGetValue(DiagnosticEditProperties.EditTypeKey, out string? editTypeString)) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| if (!properties.TryGetValue(DiagnosticEditProperties.EditPositionKey, out string? editPositionString)) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| if (!Enum.TryParse(editTypeString, out DiagnosticEditProperties.EditType editType)) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| if (!int.TryParse(editPositionString, out int editPosition)) | ||
|
Check failure on line 29 in src/CodeFixes/CodeFixContextExtensions.cs
|
||
| { | ||
| return false; | ||
| } | ||
MattKotsenas marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| editProperties = new DiagnosticEditProperties | ||
| { | ||
| TypeOfEdit = editType, | ||
| EditPosition = editPosition, | ||
| }; | ||
|
|
||
| return true; | ||
MattKotsenas marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| using System.Composition; | ||
MattKotsenas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| using Microsoft.CodeAnalysis.CodeActions; | ||
| using Microsoft.CodeAnalysis.CodeFixes; | ||
| using Microsoft.CodeAnalysis.Editing; | ||
| using Microsoft.CodeAnalysis.Simplification; | ||
|
|
||
| namespace Moq.CodeFixes; | ||
|
|
||
| /// <summary> | ||
| /// Fixes for <see cref="DiagnosticIds.SetExplicitMockBehavior"/>. | ||
| /// </summary> | ||
| [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SetExplicitMockBehaviorFixer))] | ||
| [Shared] | ||
| public class SetExplicitMockBehaviorFixer : CodeFixProvider | ||
| { | ||
| private enum BehaviorType | ||
| { | ||
| Loose, | ||
| Strict, | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(DiagnosticIds.SetExplicitMockBehavior); | ||
|
|
||
| /// <inheritdoc /> | ||
| public override FixAllProvider? GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; | ||
|
|
||
| /// <inheritdoc /> | ||
| public override async Task RegisterCodeFixesAsync(CodeFixContext context) | ||
| { | ||
| SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); | ||
| SyntaxNode? nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true); | ||
|
|
||
| if (!context.TryGetEditProperties(out DiagnosticEditProperties? editProperties)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| if (nodeToFix is null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| context.RegisterCodeFix(new SetExplicitMockBehaviorCodeAction("Set MockBehavior (Loose)", context.Document, nodeToFix, BehaviorType.Loose, editProperties.TypeOfEdit, editProperties.EditPosition), context.Diagnostics); | ||
| context.RegisterCodeFix(new SetExplicitMockBehaviorCodeAction("Set MockBehavior (Strict)", context.Document, nodeToFix, BehaviorType.Strict, editProperties.TypeOfEdit, editProperties.EditPosition), context.Diagnostics); | ||
| } | ||
MattKotsenas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| private sealed class SetExplicitMockBehaviorCodeAction : CodeAction | ||
| { | ||
| private readonly Document _document; | ||
| private readonly SyntaxNode _nodeToFix; | ||
| private readonly BehaviorType _behaviorType; | ||
| private readonly DiagnosticEditProperties.EditType _editType; | ||
| private readonly int _position; | ||
|
|
||
| public SetExplicitMockBehaviorCodeAction(string title, Document document, SyntaxNode nodeToFix, BehaviorType behaviorType, DiagnosticEditProperties.EditType editType, int position) | ||
| { | ||
| Title = title; | ||
| _document = document; | ||
| _nodeToFix = nodeToFix; | ||
| _behaviorType = behaviorType; | ||
| _editType = editType; | ||
| _position = position; | ||
| } | ||
|
|
||
| public override string Title { get; } | ||
|
|
||
| public override string? EquivalenceKey => Title; | ||
|
|
||
| protected override async Task<Document> GetChangedDocumentAsync(CancellationToken cancellationToken) | ||
| { | ||
| DocumentEditor editor = await DocumentEditor.CreateAsync(_document, cancellationToken).ConfigureAwait(false); | ||
| SemanticModel? model = await _document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); | ||
| IOperation? operation = model?.GetOperation(_nodeToFix, cancellationToken); | ||
|
|
||
| MoqKnownSymbols knownSymbols = new(editor.SemanticModel.Compilation); | ||
|
|
||
| if (knownSymbols.MockBehavior is null | ||
| || knownSymbols.MockBehaviorDefault is null | ||
| || knownSymbols.MockBehaviorLoose is null | ||
| || knownSymbols.MockBehaviorStrict is null | ||
| || operation is null) | ||
| { | ||
| return _document; | ||
| } | ||
|
|
||
| SyntaxNode behavior = _behaviorType switch | ||
| { | ||
| BehaviorType.Loose => editor.Generator.MemberAccessExpression(knownSymbols.MockBehaviorLoose), | ||
| BehaviorType.Strict => editor.Generator.MemberAccessExpression(knownSymbols.MockBehaviorStrict), | ||
| _ => throw new InvalidOperationException(), | ||
| }; | ||
|
|
||
| SyntaxNode argument = editor.Generator.Argument(behavior); | ||
|
|
||
| SyntaxNode newNode = _editType switch | ||
| { | ||
| DiagnosticEditProperties.EditType.Insert => editor.Generator.InsertArguments(operation, _position, argument), | ||
| DiagnosticEditProperties.EditType.Replace => editor.Generator.ReplaceArgument(operation, _position, argument), | ||
| _ => throw new InvalidOperationException(), | ||
| }; | ||
|
|
||
| editor.ReplaceNode(_nodeToFix, newNode.WithAdditionalAnnotations(Simplifier.Annotation)); | ||
| return editor.GetChangedDocument(); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| using Microsoft.CodeAnalysis.Editing; | ||
MattKotsenas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| namespace Moq.CodeFixes; | ||
|
|
||
| internal static class SyntaxGeneratorExtensions | ||
| { | ||
| public static SyntaxNode MemberAccessExpression(this SyntaxGenerator generator, IFieldSymbol fieldSymbol) | ||
| { | ||
| return generator.MemberAccessExpression(generator.TypeExpression(fieldSymbol.Type), generator.IdentifierName(fieldSymbol.Name)); | ||
| } | ||
|
|
||
| public static SyntaxNode InsertArguments(this SyntaxGenerator generator, IOperation operation, int index, params SyntaxNode[] items) | ||
| { | ||
| // Ideally we could modify argument lists only using the IOperation APIs, but I haven't figured out a way to do that yet. | ||
| return generator.InsertArguments(operation.Syntax, index, items); | ||
| } | ||
|
|
||
| public static SyntaxNode InsertArguments(this SyntaxGenerator generator, SyntaxNode syntax, int index, params SyntaxNode[] items) | ||
| { | ||
| if (Array.Exists(items, item => item is not ArgumentSyntax)) | ||
| { | ||
| throw new ArgumentException("Must all be of type ArgumentSyntax", nameof(items)); | ||
| } | ||
|
|
||
| if (syntax is InvocationExpressionSyntax invocation) | ||
| { | ||
| SeparatedSyntaxList<ArgumentSyntax> arguments = invocation.ArgumentList.Arguments; | ||
| arguments = arguments.InsertRange(index, items.OfType<ArgumentSyntax>()); | ||
| return invocation.WithArgumentList(SyntaxFactory.ArgumentList(arguments)); | ||
MattKotsenas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| if (syntax is ObjectCreationExpressionSyntax creation) | ||
| { | ||
| SeparatedSyntaxList<ArgumentSyntax> arguments = creation.ArgumentList?.Arguments ?? []; | ||
| arguments = arguments.InsertRange(index, items.OfType<ArgumentSyntax>()); | ||
| return creation.WithArgumentList(SyntaxFactory.ArgumentList(arguments)); | ||
| } | ||
|
|
||
| throw new ArgumentException($"Must be of type {nameof(InvocationExpressionSyntax)} or {nameof(ObjectCreationExpressionSyntax)} but is of type {syntax.GetType().Name}", nameof(syntax)); | ||
| } | ||
|
|
||
| public static SyntaxNode ReplaceArgument(this SyntaxGenerator generator, IOperation operation, int index, SyntaxNode item) | ||
| { | ||
| // Ideally we could modify argument lists only using the IOperation APIs, but I haven't figured out a way to do that yet. | ||
| return generator.ReplaceArgument(operation.Syntax, index, item); | ||
| } | ||
|
|
||
| public static SyntaxNode ReplaceArgument(this SyntaxGenerator generator, SyntaxNode syntax, int index, SyntaxNode item) | ||
| { | ||
| if (item is not ArgumentSyntax argument) | ||
| { | ||
| throw new ArgumentException("Must be of type ArgumentSyntax", nameof(item)); | ||
| } | ||
|
|
||
| if (syntax is InvocationExpressionSyntax invocation) | ||
| { | ||
| SeparatedSyntaxList<ArgumentSyntax> arguments = invocation.ArgumentList.Arguments; | ||
| arguments = arguments.RemoveAt(index).Insert(index, argument); | ||
| return invocation.WithArgumentList(SyntaxFactory.ArgumentList(arguments)); | ||
| } | ||
|
|
||
| if (syntax is ObjectCreationExpressionSyntax creation) | ||
| { | ||
| SeparatedSyntaxList<ArgumentSyntax> arguments = creation.ArgumentList?.Arguments ?? []; | ||
| arguments = arguments.RemoveAt(index).Insert(index, argument); | ||
| return creation.WithArgumentList(SyntaxFactory.ArgumentList(arguments)); | ||
MattKotsenas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| throw new ArgumentException($"Must be of type {nameof(InvocationExpressionSyntax)} or {nameof(ObjectCreationExpressionSyntax)} but is of type {syntax.GetType().Name}", nameof(syntax)); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.