Skip to content

Commit

Permalink
GH-128 - code fix provider for NS400
Browse files Browse the repository at this point in the history
  • Loading branch information
tpodolak committed Jan 26, 2020
1 parent 013b132 commit e3c1ec5
Show file tree
Hide file tree
Showing 15 changed files with 1,306 additions and 1 deletion.
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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;

namespace NSubstitute.Analyzers.Shared.CodeFixProviders
{
Expand Down
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;
}
}
}
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);
}
}
}
Loading

0 comments on commit e3c1ec5

Please sign in to comment.