Skip to content
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

Gh 138 introduce substitute #141

Merged
merged 1 commit into from
Mar 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions documentation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,11 @@ See [rules list](rules/README.md).

* [Configuration](Configuration.md)
* [Visual Studio compatibility](Compatibility.md)

### Code refactoring

#### Introduce substitute

Automatically create substitutes for constructor arguments as fields or local variables.
![introducesubstitutecsharp](https://user-images.githubusercontent.com/7378346/77374918-54832d80-6d6c-11ea-965a-eb96080bf8cd.gif)
![introducesubstitutevisualbasic](https://user-images.githubusercontent.com/7378346/77374922-564cf100-6d6c-11ea-8fca-96d8a9724b36.gif)
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using NSubstitute.Analyzers.Shared.CodeRefactoringProviders;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace NSubstitute.Analyzers.CSharp.CodeRefactoringProviders
{
[ExportCodeRefactoringProvider(LanguageNames.CSharp)]
internal sealed class IntroduceSubstituteCodeRefactoringProvider : AbstractIntroduceSubstituteCodeRefactoringProvider<ObjectCreationExpressionSyntax, ArgumentListSyntax, ArgumentSyntax>
{
protected override IReadOnlyList<ArgumentSyntax> GetArgumentSyntaxNodes(ArgumentListSyntax argumentListSyntax, TextSpan span)
{
return argumentListSyntax.Arguments;
}

protected override ObjectCreationExpressionSyntax UpdateObjectCreationExpression(
ObjectCreationExpressionSyntax objectCreationExpressionSyntax,
IReadOnlyList<ArgumentSyntax> updatedArguments)
{
var originalArgumentList = objectCreationExpressionSyntax.ArgumentList;
var updatedArgumentList = originalArgumentList.Update(
originalArgumentList.OpenParenToken.WithTrailingTrivia(),
SeparatedList(updatedArguments),
originalArgumentList.CloseParenToken);

updatedArgumentList = UpdateArgumentListTrivia(originalArgumentList, updatedArgumentList);

return objectCreationExpressionSyntax.WithArgumentList(updatedArgumentList);
}

protected override SyntaxNode FindSiblingNodeForLocalSubstitute(ObjectCreationExpressionSyntax creationExpression)
{
var container = creationExpression.Ancestors()
.FirstOrDefault(ancestor => ancestor.Kind() == SyntaxKind.Block);

return container?.ChildNodes().FirstOrDefault();
}

protected override SyntaxNode FindSiblingNodeForReadonlySubstitute(SyntaxNode creationExpression)
{
var typeDeclarationSyntax = creationExpression.Ancestors()
.OfType<TypeDeclarationSyntax>()
.FirstOrDefault();

return typeDeclarationSyntax?.Members.FirstOrDefault();
}

private static ArgumentListSyntax UpdateArgumentListTrivia(
ArgumentListSyntax originalArgumentList,
ArgumentListSyntax updatedArgumentList)
{
if (originalArgumentList.CloseParenToken.IsMissing == false)
{
return updatedArgumentList;
}

var token = originalArgumentList.ChildTokens().LastOrDefault(innerToken => innerToken.IsMissing == false);

if (token.Kind() != SyntaxKind.None && token.HasTrailingTrivia)
{
updatedArgumentList = updatedArgumentList.WithTrailingTrivia(token.TrailingTrivia);
}

return updatedArgumentList;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Text;
using NSubstitute.Analyzers.Shared.Extensions;

namespace NSubstitute.Analyzers.Shared.CodeRefactoringProviders
{
internal abstract class AbstractIntroduceSubstituteCodeRefactoringProvider<TObjectCreationExpressionSyntax, TArgumentListSyntax, TArgumentSyntax> : CodeRefactoringProvider
where TObjectCreationExpressionSyntax : SyntaxNode
where TArgumentListSyntax : SyntaxNode
where TArgumentSyntax : SyntaxNode
{
public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var node = root.FindNode(context.Span);

if (!(node is TArgumentListSyntax argumentListSyntax) ||
!(node.Parent is TObjectCreationExpressionSyntax objectCreationExpressionSyntax))
{
return;
}

var semanticModel = await context.Document.GetSemanticModelAsync();
var refactoringActions = CreateRefactoringActions(
context,
semanticModel,
objectCreationExpressionSyntax,
argumentListSyntax);

foreach (var refactoringAction in refactoringActions)
{
context.RegisterRefactoring(refactoringAction);
}
}

protected abstract IReadOnlyList<TArgumentSyntax> GetArgumentSyntaxNodes(TArgumentListSyntax argumentListSyntax, TextSpan span);

protected abstract TObjectCreationExpressionSyntax UpdateObjectCreationExpression(
TObjectCreationExpressionSyntax objectCreationExpressionSyntax,
IReadOnlyList<TArgumentSyntax> argumentSyntax);

protected virtual bool IsMissing(TArgumentSyntax argumentSyntax) => argumentSyntax.IsMissing;

protected abstract SyntaxNode FindSiblingNodeForLocalSubstitute(TObjectCreationExpressionSyntax creationExpression);

protected abstract SyntaxNode FindSiblingNodeForReadonlySubstitute(SyntaxNode creationExpression);

private IEnumerable<CodeAction> CreateRefactoringActions(
CodeRefactoringContext context,
SemanticModel semanticModel,
TObjectCreationExpressionSyntax objectCreationExpressionSyntax,
TArgumentListSyntax argumentListSyntax)
{
var constructorSymbol = GetKnownConstructorSymbol(semanticModel, objectCreationExpressionSyntax);

if (constructorSymbol == null || constructorSymbol.Parameters.Length == 0)
{
yield break;
}

var existingArguments = GetArgumentSyntaxNodes(argumentListSyntax, context.Span);
var constructorParameters = constructorSymbol.Parameters.OrderBy(parameter => parameter.Ordinal).ToList();

var missingArgumentsPositions = GetMissingArgumentsPositions(existingArguments, constructorParameters);
if (missingArgumentsPositions.Count == 0)
{
yield break;
}

var localSubstituteSiblingNode = FindSiblingNodeForLocalSubstitute(objectCreationExpressionSyntax);
var readonlySubstituteSiblingNode = FindSiblingNodeForReadonlySubstitute(objectCreationExpressionSyntax);

var argumentIndexAtSpan = FindArgumentIndexAtSpan(existingArguments, context.Span);
if (missingArgumentsPositions.Contains(argumentIndexAtSpan))
{
var substituteName = constructorParameters[argumentIndexAtSpan].ToMinimalSymbolString(semanticModel);

if (localSubstituteSiblingNode != null)
{
yield return CodeAction.Create(
$"Introduce local substitute for {substituteName}",
token => IntroduceLocalSubstitute(
context,
objectCreationExpressionSyntax,
existingArguments,
constructorParameters,
new[] { argumentIndexAtSpan },
localSubstituteSiblingNode));
}

if (readonlySubstituteSiblingNode != null)
{
yield return CodeAction.Create(
$"Introduce readonly substitute for {substituteName}",
token => IntroduceReadonlySubstitute(
context,
objectCreationExpressionSyntax,
existingArguments,
constructorParameters,
new[] { argumentIndexAtSpan },
readonlySubstituteSiblingNode));
}
}

if (localSubstituteSiblingNode != null)
{
yield return CodeAction.Create(
"Introduce local substitutes for missing arguments",
token => IntroduceLocalSubstitute(
context,
objectCreationExpressionSyntax,
existingArguments,
constructorParameters,
missingArgumentsPositions,
localSubstituteSiblingNode));
}

if (readonlySubstituteSiblingNode != null)
{
yield return CodeAction.Create(
"Introduce readonly substitutes for missing arguments",
token => IntroduceReadonlySubstitute(
context,
objectCreationExpressionSyntax,
existingArguments,
constructorParameters,
missingArgumentsPositions,
readonlySubstituteSiblingNode));
}
}

private async Task<Document> IntroduceReadonlySubstitute(
CodeRefactoringContext context,
TObjectCreationExpressionSyntax objectCreationExpressionSyntax,
IReadOnlyList<TArgumentSyntax> existingArguments,
IReadOnlyList<IParameterSymbol> constructorParameters,
IReadOnlyList<int> missingArgumentsPositions,
SyntaxNode siblingNode)
{
SyntaxNode CreateFieldDeclaration(SyntaxGenerator syntaxGenerator, IParameterSymbol parameterSymbol, SyntaxNode invocationExpression)
{
return syntaxGenerator.FieldDeclaration(
parameterSymbol.Name,
syntaxGenerator.TypeExpression(parameterSymbol.Type),
Accessibility.Private,
DeclarationModifiers.ReadOnly,
invocationExpression);
}

return await IntroduceSubstitute(
context,
objectCreationExpressionSyntax,
existingArguments,
constructorParameters,
missingArgumentsPositions,
siblingNode,
CreateFieldDeclaration);
}

private async Task<Document> IntroduceLocalSubstitute(
CodeRefactoringContext context,
TObjectCreationExpressionSyntax objectCreationExpressionSyntax,
IReadOnlyList<TArgumentSyntax> existingArguments,
IReadOnlyList<IParameterSymbol> constructorParameters,
IReadOnlyList<int> missingArgumentsPositions,
SyntaxNode siblingNode)
{
SyntaxNode CreateLocalDeclaration(SyntaxGenerator syntaxGenerator, IParameterSymbol parameterSymbol, SyntaxNode invocationExpression)
{
return syntaxGenerator.LocalDeclarationStatement(parameterSymbol.Name, invocationExpression);
}

return await IntroduceSubstitute(
context,
objectCreationExpressionSyntax,
existingArguments,
constructorParameters,
missingArgumentsPositions,
siblingNode,
CreateLocalDeclaration);
}

private async Task<Document> IntroduceSubstitute(
CodeRefactoringContext context,
TObjectCreationExpressionSyntax objectCreationExpressionSyntax,
IReadOnlyList<TArgumentSyntax> existingArguments,
IReadOnlyList<IParameterSymbol> constructorParameters,
IReadOnlyList<int> missingArgumentsPositions,
SyntaxNode siblingNode,
Func<SyntaxGenerator, IParameterSymbol, SyntaxNode, SyntaxNode> declarationFactory)
{
var documentEditor = await DocumentEditor.CreateAsync(context.Document);
var syntaxGenerator = documentEditor.Generator;
var declarations = new List<SyntaxNode>();
var newArgumentList = new List<TArgumentSyntax>();

for (var parameterPosition = 0; parameterPosition < constructorParameters.Count; parameterPosition++)
{
var parameterSymbol = constructorParameters[parameterPosition];
if (missingArgumentsPositions.Contains(parameterPosition))
{
var invocationExpression = syntaxGenerator.SubstituteForInvocationExpression(parameterSymbol);

var declaration = declarationFactory(
syntaxGenerator,
parameterSymbol,
invocationExpression);

declarations.Add(declaration);

newArgumentList.Add((TArgumentSyntax)syntaxGenerator.Argument(syntaxGenerator.IdentifierName(parameterSymbol.Name)));
}
else if (parameterPosition < existingArguments.Count)
{
newArgumentList.Add(existingArguments[parameterPosition]);
}
}

documentEditor.InsertBefore(siblingNode, declarations);

documentEditor.ReplaceNode(
objectCreationExpressionSyntax,
UpdateObjectCreationExpression(objectCreationExpressionSyntax, newArgumentList));

return documentEditor.GetChangedDocument();
}

private IMethodSymbol GetKnownConstructorSymbol(SemanticModel semanticModel, TObjectCreationExpressionSyntax objectCreationExpressionSyntax)
{
var symbol = semanticModel.GetSymbolInfo(objectCreationExpressionSyntax);

if (symbol.Symbol is IMethodSymbol methodSymbol)
{
return methodSymbol;
}

var candidateMethodSymbols = symbol.CandidateSymbols.OfType<IMethodSymbol>().ToList();

if (candidateMethodSymbols.Count == 0 || candidateMethodSymbols.Count > 1)
{
return null;
}

return candidateMethodSymbols.Single();
}

private IReadOnlyList<int> GetMissingArgumentsPositions(
IReadOnlyList<TArgumentSyntax> argumentSyntaxNodes,
IReadOnlyList<IParameterSymbol> parameterSymbols)
{
if (parameterSymbols.Count == 0)
{
return Array.Empty<int>();
}

if (argumentSyntaxNodes.Count == 0)
{
return Enumerable.Range(0, parameterSymbols.Count).ToList();
}

var result = new List<int>();
for (var symbolPosition = 0; symbolPosition < parameterSymbols.Count; symbolPosition++)
{
if (symbolPosition >= argumentSyntaxNodes.Count || IsMissing(argumentSyntaxNodes[symbolPosition]))
{
result.Add(symbolPosition);
}
}

return result;
}

private int FindArgumentIndexAtSpan(IReadOnlyList<TArgumentSyntax> argumentSyntaxNodes, TextSpan span)
{
var findArgumentIndexAtSpan = argumentSyntaxNodes.IndexOf(node => node.FullSpan.IntersectsWith(span));
return findArgumentIndexAtSpan >= 0 ? findArgumentIndexAtSpan : 0;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;

namespace NSubstitute.Analyzers.Shared.Extensions
{
internal static class IEnumerableExtensions
{
public static int IndexOf<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
var index = -1;
foreach (var item in source)
{
index++;
if (predicate(item))
{
return index;
}
}

return index;
}
}
}
Loading