-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
GH-128 - code fix provider for NS400
- Loading branch information
Showing
15 changed files
with
1,306 additions
and
1 deletion.
There are no files selected for viewing
119 changes: 119 additions & 0 deletions
119
src/NSubstitute.Analyzers.CSharp/CodeFixProviders/ReEntrantSetupCodeFixProvider.cs
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,119 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CodeFixes; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Microsoft.CodeAnalysis.Editing; | ||
using Microsoft.CodeAnalysis.Simplification; | ||
using NSubstitute.Analyzers.CSharp.Extensions; | ||
using NSubstitute.Analyzers.Shared.CodeFixProviders; | ||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; | ||
|
||
namespace NSubstitute.Analyzers.CSharp.CodeFixProviders | ||
{ | ||
[ExportCodeFixProvider(LanguageNames.CSharp)] | ||
internal sealed class ReEntrantSetupCodeFixProvider : AbstractReEntrantSetupCodeFixProvider<ArgumentListSyntax, ArgumentSyntax> | ||
{ | ||
protected override int AwaitExpressionRawKind { get; } = (int)SyntaxKind.AwaitExpression; | ||
|
||
protected override ArgumentSyntax CreateUpdatedArgumentSyntaxNode(ArgumentSyntax argumentSyntaxNode) | ||
{ | ||
return argumentSyntaxNode.WithExpression(CreateSimpleLambdaExpressionNode(argumentSyntaxNode.Expression)); | ||
} | ||
|
||
protected override IEnumerable<SyntaxNode> GetParameterExpressionsFromArrayArgument(ArgumentSyntax argumentSyntaxNode) | ||
{ | ||
return argumentSyntaxNode.Expression.GetParameterExpressionsFromArrayArgument()?.Select<ExpressionSyntax, SyntaxNode>(syntax => syntax); | ||
} | ||
|
||
protected override ArgumentSyntax CreateUpdatedParamsArgumentSyntaxNode( | ||
SyntaxGenerator syntaxGenerator, | ||
ITypeSymbol typeSymbol, | ||
ArgumentSyntax argumentSyntaxNode) | ||
{ | ||
var expression = argumentSyntaxNode.Expression; | ||
ArrayCreationExpressionSyntax resultArrayCreationExpressionSyntax; | ||
|
||
switch (expression) | ||
{ | ||
case ArrayCreationExpressionSyntax arrayCreationExpressionSyntax: | ||
resultArrayCreationExpressionSyntax = CreateArrayCreationExpression( | ||
syntaxGenerator, | ||
typeSymbol, | ||
arrayCreationExpressionSyntax.Initializer); | ||
break; | ||
case ImplicitArrayCreationExpressionSyntax implicitArrayCreationExpressionSyntax: | ||
resultArrayCreationExpressionSyntax = CreateArrayCreationExpression( | ||
syntaxGenerator, | ||
typeSymbol, | ||
implicitArrayCreationExpressionSyntax.Initializer); | ||
break; | ||
default: | ||
throw new ArgumentException($"{argumentSyntaxNode.Kind()} is not recognized as array initialization", nameof(argumentSyntaxNode)); | ||
} | ||
|
||
return argumentSyntaxNode.WithExpression(resultArrayCreationExpressionSyntax); | ||
} | ||
|
||
protected override IEnumerable<ArgumentSyntax> GetArguments(ArgumentListSyntax argumentSyntax) | ||
{ | ||
return argumentSyntax.Arguments; | ||
} | ||
|
||
protected override SyntaxNode GetArgumentExpressionSyntax(ArgumentSyntax argumentSyntax) | ||
{ | ||
return argumentSyntax.Expression; | ||
} | ||
|
||
private static SimpleLambdaExpressionSyntax CreateSimpleLambdaExpressionNode(SyntaxNode content) | ||
{ | ||
return SimpleLambdaExpression( | ||
Parameter(Identifier("_").WithTrailingTrivia(Space)), | ||
(CSharpSyntaxNode)content.WithLeadingTrivia(Space)); | ||
} | ||
|
||
private static ArrayCreationExpressionSyntax CreateArrayCreationExpression( | ||
SyntaxGenerator syntaxGenerator, | ||
ITypeSymbol typeSymbol, | ||
InitializerExpressionSyntax initializerExpressionSyntax) | ||
{ | ||
var arrayType = CreateArrayTypeNode(syntaxGenerator, typeSymbol); | ||
var syntaxes = CreateSimpleLambdaExpressions(initializerExpressionSyntax); | ||
|
||
var initializer = InitializerExpression(SyntaxKind.ArrayInitializerExpression, syntaxes); | ||
|
||
return ArrayCreationExpression(arrayType, initializer); | ||
} | ||
|
||
private static SeparatedSyntaxList<ExpressionSyntax> CreateSimpleLambdaExpressions(InitializerExpressionSyntax initializerExpressionSyntax) | ||
{ | ||
var expressions = initializerExpressionSyntax.Expressions.Select<ExpressionSyntax, ExpressionSyntax>(CreateSimpleLambdaExpressionNode); | ||
return SeparatedList(expressions); | ||
} | ||
|
||
private static ArrayTypeSyntax CreateArrayTypeNode(SyntaxGenerator syntaxGenerator, ITypeSymbol type) | ||
{ | ||
var typeSyntax = (TypeSyntax)syntaxGenerator.TypeExpression(type); | ||
var typeArgumentListSyntax = TypeArgumentList( | ||
SeparatedList<TypeSyntax>( | ||
new SyntaxNodeOrToken[] | ||
{ | ||
QualifiedName( | ||
QualifiedName( | ||
IdentifierName("NSubstitute"), | ||
IdentifierName("Core")), | ||
IdentifierName("CallInfo")), | ||
Token(SyntaxKind.CommaToken), | ||
typeSyntax | ||
})); | ||
|
||
var qualifiedNameSyntax = QualifiedName(IdentifierName("System"), GenericName(Identifier("Func"), typeArgumentListSyntax)); | ||
|
||
var arrayRankSpecifierSyntaxes = SingletonList(ArrayRankSpecifier(SingletonSeparatedList<ExpressionSyntax>(OmittedArraySizeExpression()))); | ||
|
||
return ArrayType(qualifiedNameSyntax, arrayRankSpecifierSyntaxes).WithAdditionalAnnotations(Simplifier.Annotation); | ||
} | ||
} | ||
} |
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
127 changes: 127 additions & 0 deletions
127
src/NSubstitute.Analyzers.Shared/CodeFixProviders/AbstractReEntrantSetupCodeFixProvider.cs
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,127 @@ | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Linq; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CodeActions; | ||
using Microsoft.CodeAnalysis.CodeFixes; | ||
using Microsoft.CodeAnalysis.Editing; | ||
using Microsoft.CodeAnalysis.Operations; | ||
using Microsoft.CodeAnalysis.Simplification; | ||
|
||
namespace NSubstitute.Analyzers.Shared.CodeFixProviders | ||
{ | ||
internal abstract class AbstractReEntrantSetupCodeFixProvider<TArgumentListSyntax, TArgumentSyntax> : CodeFixProvider | ||
where TArgumentListSyntax : SyntaxNode | ||
where TArgumentSyntax : SyntaxNode | ||
{ | ||
public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(DiagnosticIdentifiers.ReEntrantSubstituteCall); | ||
|
||
protected abstract TArgumentSyntax CreateUpdatedArgumentSyntaxNode(TArgumentSyntax argumentSyntaxNode); | ||
|
||
protected abstract TArgumentSyntax CreateUpdatedParamsArgumentSyntaxNode(SyntaxGenerator syntaxGenerator, ITypeSymbol returnedTypeSymbol, TArgumentSyntax argumentSyntaxNode); | ||
|
||
protected abstract SyntaxNode GetArgumentExpressionSyntax(TArgumentSyntax argumentSyntax); | ||
|
||
protected abstract IEnumerable<SyntaxNode> GetParameterExpressionsFromArrayArgument(TArgumentSyntax argumentSyntaxNode); | ||
|
||
protected abstract int AwaitExpressionRawKind { get; } | ||
|
||
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) | ||
{ | ||
var diagnostic = context.Diagnostics.FirstOrDefault(diag => | ||
diag.Descriptor.Id == DiagnosticIdentifiers.ReEntrantSubstituteCall); | ||
|
||
if (diagnostic == null) | ||
{ | ||
return; | ||
} | ||
|
||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken); | ||
var node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); | ||
var semanticModel = await context.Document.GetSemanticModelAsync(); | ||
|
||
var argumentList = GetArgumentListSyntax(node); | ||
var allArguments = GetArguments(argumentList).ToList(); | ||
|
||
if (IsFixSupported(semanticModel, allArguments) == false) | ||
{ | ||
return; | ||
} | ||
|
||
var codeAction = CodeAction.Create( | ||
"Replace with lambda", | ||
ct => CreateChangedDocument(context, argumentList, allArguments, ct), | ||
nameof(AbstractReEntrantSetupCodeFixProvider<TArgumentListSyntax, TArgumentSyntax>)); | ||
|
||
context.RegisterCodeFix(codeAction, diagnostic); | ||
} | ||
|
||
protected abstract IEnumerable<TArgumentSyntax> GetArguments(TArgumentListSyntax argumentSyntax); | ||
|
||
private async Task<Document> CreateChangedDocument( | ||
CodeFixContext context, | ||
TArgumentListSyntax argumentListSyntax, | ||
IReadOnlyList<TArgumentSyntax> argumentSyntaxes, | ||
CancellationToken ct) | ||
{ | ||
var documentEditor = await DocumentEditor.CreateAsync(context.Document, ct); | ||
var semanticModel = await context.Document.GetSemanticModelAsync(ct); | ||
var invocationSyntaxNode = argumentListSyntax.Parent; | ||
if (!(semanticModel.GetSymbolInfo(invocationSyntaxNode).Symbol is IMethodSymbol methodSymbol)) | ||
{ | ||
return context.Document; | ||
} | ||
|
||
var skip = methodSymbol.MethodKind == MethodKind.Ordinary | ||
? 1 | ||
: 0; | ||
|
||
foreach (var argumentSyntax in argumentSyntaxes.Skip(skip)) | ||
{ | ||
if (IsArrayParamsArgument(semanticModel, argumentSyntax)) | ||
{ | ||
var updatedParamsArgumentSyntaxNode = CreateUpdatedParamsArgumentSyntaxNode( | ||
SyntaxGenerator.GetGenerator(context.Document), | ||
methodSymbol.TypeArguments.FirstOrDefault() ?? methodSymbol.ReceiverType, | ||
argumentSyntax); | ||
|
||
documentEditor.ReplaceNode(argumentSyntax, updatedParamsArgumentSyntaxNode); | ||
} | ||
else | ||
{ | ||
var updatedArgumentSyntax = CreateUpdatedArgumentSyntaxNode(argumentSyntax); | ||
|
||
documentEditor.ReplaceNode(argumentSyntax, updatedArgumentSyntax); | ||
} | ||
} | ||
|
||
return await Simplifier.ReduceAsync(documentEditor.GetChangedDocument(), cancellationToken: ct); | ||
} | ||
|
||
private bool IsFixSupported(SemanticModel semanticModel, IEnumerable<TArgumentSyntax> arguments) | ||
{ | ||
return arguments.All(arg => | ||
{ | ||
var expressionSyntax = GetArgumentExpressionSyntax(arg); | ||
var arrayExpressions = GetParameterExpressionsFromArrayArgument(arg); | ||
return expressionSyntax.RawKind != AwaitExpressionRawKind && | ||
(IsArrayParamsArgument(semanticModel, arg) == false || (arrayExpressions != null && arrayExpressions.All(exp => exp.RawKind != AwaitExpressionRawKind))); | ||
}); | ||
} | ||
|
||
private bool IsArrayParamsArgument(SemanticModel semanticModel, TArgumentSyntax argumentSyntax) | ||
{ | ||
var operation = semanticModel.GetOperation(argumentSyntax); | ||
return operation is IArgumentOperation argumentOperation && argumentOperation.Parameter.IsParams; | ||
} | ||
|
||
private static TArgumentListSyntax GetArgumentListSyntax(SyntaxNode diagnosticNode) | ||
{ | ||
var argumentListSyntax = diagnosticNode.Ancestors().OfType<TArgumentListSyntax>().FirstOrDefault(); | ||
return argumentListSyntax; | ||
} | ||
} | ||
} |
140 changes: 140 additions & 0 deletions
140
src/NSubstitute.Analyzers.VisualBasic/CodeFixProviders/ReEntrantSetupCodeFixProvider.cs
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,140 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CodeFixes; | ||
using Microsoft.CodeAnalysis.Editing; | ||
using Microsoft.CodeAnalysis.Simplification; | ||
using Microsoft.CodeAnalysis.VisualBasic; | ||
using Microsoft.CodeAnalysis.VisualBasic.Syntax; | ||
using NSubstitute.Analyzers.Shared.CodeFixProviders; | ||
using NSubstitute.Analyzers.VisualBasic.Extensions; | ||
using static Microsoft.CodeAnalysis.VisualBasic.SyntaxFactory; | ||
|
||
namespace NSubstitute.Analyzers.VisualBasic.CodeFixProviders | ||
{ | ||
[ExportCodeFixProvider(LanguageNames.VisualBasic)] | ||
internal sealed class ReEntrantSetupCodeFixProvider : AbstractReEntrantSetupCodeFixProvider<ArgumentListSyntax, ArgumentSyntax> | ||
{ | ||
protected override ArgumentSyntax CreateUpdatedArgumentSyntaxNode(ArgumentSyntax argumentSyntaxNode) | ||
{ | ||
var expressionSyntax = argumentSyntaxNode.GetExpression(); | ||
|
||
var lambdaExpression = CreateSingleLineLambdaExpression(expressionSyntax); | ||
|
||
switch (argumentSyntaxNode) | ||
{ | ||
case SimpleArgumentSyntax simpleArgumentSyntax: | ||
return simpleArgumentSyntax.WithExpression(lambdaExpression); | ||
} | ||
|
||
return argumentSyntaxNode; | ||
} | ||
|
||
protected override ArgumentSyntax CreateUpdatedParamsArgumentSyntaxNode(SyntaxGenerator syntaxGenerator, ITypeSymbol typeSymbol, ArgumentSyntax argumentSyntaxNode) | ||
{ | ||
if (!(argumentSyntaxNode is SimpleArgumentSyntax simpleArgumentSyntax)) | ||
{ | ||
return argumentSyntaxNode; | ||
} | ||
|
||
var expression = argumentSyntaxNode.GetExpression(); | ||
ArrayCreationExpressionSyntax resultArrayCreationExpressionSyntax; | ||
|
||
switch (expression) | ||
{ | ||
case ArrayCreationExpressionSyntax arrayCreationExpressionSyntax: | ||
resultArrayCreationExpressionSyntax = CreateArrayCreationExpression( | ||
syntaxGenerator, | ||
typeSymbol, | ||
arrayCreationExpressionSyntax.Initializer.Initializers); | ||
break; | ||
case CollectionInitializerSyntax implicitArrayCreationExpressionSyntax: | ||
resultArrayCreationExpressionSyntax = CreateArrayCreationExpression( | ||
syntaxGenerator, | ||
typeSymbol, | ||
implicitArrayCreationExpressionSyntax.Initializers); | ||
break; | ||
default: | ||
throw new ArgumentException($"{argumentSyntaxNode.Kind()} is not recognized as array initialization", nameof(argumentSyntaxNode)); | ||
} | ||
|
||
return simpleArgumentSyntax.WithExpression(resultArrayCreationExpressionSyntax); | ||
} | ||
|
||
protected override SyntaxNode GetArgumentExpressionSyntax(ArgumentSyntax argumentSyntax) | ||
{ | ||
return argumentSyntax.GetExpression(); | ||
} | ||
|
||
protected override IEnumerable<SyntaxNode> GetParameterExpressionsFromArrayArgument(ArgumentSyntax argumentSyntaxNode) | ||
{ | ||
return argumentSyntaxNode.GetExpression().GetParameterExpressionsFromArrayArgument()?.Select<ExpressionSyntax, SyntaxNode>(syntax => syntax); | ||
} | ||
|
||
protected override int AwaitExpressionRawKind { get; } = (int)SyntaxKind.AwaitExpression; | ||
|
||
protected override IEnumerable<ArgumentSyntax> GetArguments(ArgumentListSyntax argumentSyntax) | ||
{ | ||
return argumentSyntax.Arguments; | ||
} | ||
|
||
private static ArrayCreationExpressionSyntax CreateArrayCreationExpression( | ||
SyntaxGenerator syntaxGenerator, | ||
ITypeSymbol typeSymbol, | ||
SeparatedSyntaxList<ExpressionSyntax> initializers) | ||
{ | ||
var typeNode = CreateTypeNode(syntaxGenerator, typeSymbol); | ||
var syntaxes = CreateSingleLineLambdaExpressions(initializers); | ||
|
||
var initializer = CollectionInitializer(syntaxes); | ||
var arrayRankSpecifierSyntaxes = SingletonList(ArrayRankSpecifier()); | ||
|
||
return ArrayCreationExpression(Token(SyntaxKind.NewKeyword), new SyntaxList<AttributeListSyntax>(), typeNode, null, arrayRankSpecifierSyntaxes, initializer); | ||
} | ||
|
||
private static SeparatedSyntaxList<ExpressionSyntax> CreateSingleLineLambdaExpressions(SeparatedSyntaxList<ExpressionSyntax> expressions) | ||
{ | ||
var singleLineLambdaExpressionSyntaxes = expressions.Select<ExpressionSyntax, ExpressionSyntax>(CreateSingleLineLambdaExpression); | ||
|
||
return SeparatedList(singleLineLambdaExpressionSyntaxes); | ||
} | ||
|
||
private static SingleLineLambdaExpressionSyntax CreateSingleLineLambdaExpression(ExpressionSyntax expressionSyntax) | ||
{ | ||
var separatedSyntaxList = SeparatedList(SingletonList(Parameter(ModifiedIdentifier(Identifier("x"))))); | ||
var functionLambdaHeader = FunctionLambdaHeader( | ||
new SyntaxList<AttributeListSyntax>(), | ||
TokenList(), | ||
ParameterList(separatedSyntaxList), | ||
null); | ||
|
||
var lambdaExpression = SingleLineLambdaExpression( | ||
SyntaxKind.SingleLineFunctionLambdaExpression, | ||
functionLambdaHeader, | ||
expressionSyntax.WithLeadingTrivia()); | ||
return lambdaExpression; | ||
} | ||
|
||
private static QualifiedNameSyntax CreateTypeNode(SyntaxGenerator syntaxGenerator, ITypeSymbol type) | ||
{ | ||
var typeSyntax = (TypeSyntax)syntaxGenerator.TypeExpression(type); | ||
var typeArgumentListSyntax = TypeArgumentList( | ||
SeparatedList<TypeSyntax>( | ||
new SyntaxNodeOrToken[] | ||
{ | ||
QualifiedName( | ||
QualifiedName( | ||
IdentifierName("NSubstitute"), | ||
IdentifierName("Core")), | ||
IdentifierName("CallInfo")), | ||
Token(SyntaxKind.CommaToken), | ||
typeSyntax | ||
})); | ||
|
||
var qualifiedNameSyntax = QualifiedName(IdentifierName("System"), GenericName(Identifier("Func"), typeArgumentListSyntax)); | ||
|
||
return qualifiedNameSyntax.WithAdditionalAnnotations(Simplifier.Annotation); | ||
} | ||
} | ||
} |
Oops, something went wrong.