-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor SetExplicitMockBehaviorAnalyzer to use KnownSymbols and add …
…CodeFix (#296) This PR accomplishes two goals: 1. It refactors the `SetExplicitMockBehaviorAnalyzer` to use `KnownSymbols` to simplify the analyzer and use the `IOperation`-based analysis 2. It adds a corresponding code fixer that provides two entries under the "lightbulb" - Set MockBehavior (Loose) - Set MockBehavior (Strict)
- Loading branch information
1 parent
2cec952
commit f80520f
Showing
15 changed files
with
675 additions
and
126 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
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,14 @@ | ||
using System.Diagnostics.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CodeFixes; | ||
|
||
namespace Moq.CodeFixes; | ||
|
||
internal static class CodeFixContextExtensions | ||
{ | ||
public static bool TryGetEditProperties(this CodeFixContext context, [NotNullWhen(true)] out DiagnosticEditProperties? editProperties) | ||
{ | ||
ImmutableDictionary<string, string?> properties = context.Diagnostics[0].Properties; | ||
|
||
return DiagnosticEditProperties.TryGetFromImmutableDictionary(properties, out editProperties); | ||
} | ||
} |
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,107 @@ | ||
using System.Composition; | ||
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); | ||
} | ||
|
||
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(); | ||
} | ||
} | ||
} |
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,71 @@ | ||
using Microsoft.CodeAnalysis.Editing; | ||
|
||
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)); | ||
} | ||
|
||
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)); | ||
} | ||
|
||
throw new ArgumentException($"Must be of type {nameof(InvocationExpressionSyntax)} or {nameof(ObjectCreationExpressionSyntax)} but is of type {syntax.GetType().Name}", nameof(syntax)); | ||
} | ||
} |
Oops, something went wrong.