diff --git a/documentation/README.md b/documentation/README.md index da2d67e6..47a654d4 100644 --- a/documentation/README.md +++ b/documentation/README.md @@ -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) diff --git a/src/NSubstitute.Analyzers.CSharp/CodeRefactoringProviders/IntroduceSubstituteCodeRefactoringProvider.cs b/src/NSubstitute.Analyzers.CSharp/CodeRefactoringProviders/IntroduceSubstituteCodeRefactoringProvider.cs new file mode 100644 index 00000000..df9cda44 --- /dev/null +++ b/src/NSubstitute.Analyzers.CSharp/CodeRefactoringProviders/IntroduceSubstituteCodeRefactoringProvider.cs @@ -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 + { + protected override IReadOnlyList GetArgumentSyntaxNodes(ArgumentListSyntax argumentListSyntax, TextSpan span) + { + return argumentListSyntax.Arguments; + } + + protected override ObjectCreationExpressionSyntax UpdateObjectCreationExpression( + ObjectCreationExpressionSyntax objectCreationExpressionSyntax, + IReadOnlyList 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() + .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; + } + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/CodeRefactoringProviders/AbstractIntroduceSubstituteCodeRefactoringProvider.cs b/src/NSubstitute.Analyzers.Shared/CodeRefactoringProviders/AbstractIntroduceSubstituteCodeRefactoringProvider.cs new file mode 100644 index 00000000..7a4bf213 --- /dev/null +++ b/src/NSubstitute.Analyzers.Shared/CodeRefactoringProviders/AbstractIntroduceSubstituteCodeRefactoringProvider.cs @@ -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 : 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 GetArgumentSyntaxNodes(TArgumentListSyntax argumentListSyntax, TextSpan span); + + protected abstract TObjectCreationExpressionSyntax UpdateObjectCreationExpression( + TObjectCreationExpressionSyntax objectCreationExpressionSyntax, + IReadOnlyList argumentSyntax); + + protected virtual bool IsMissing(TArgumentSyntax argumentSyntax) => argumentSyntax.IsMissing; + + protected abstract SyntaxNode FindSiblingNodeForLocalSubstitute(TObjectCreationExpressionSyntax creationExpression); + + protected abstract SyntaxNode FindSiblingNodeForReadonlySubstitute(SyntaxNode creationExpression); + + private IEnumerable 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 IntroduceReadonlySubstitute( + CodeRefactoringContext context, + TObjectCreationExpressionSyntax objectCreationExpressionSyntax, + IReadOnlyList existingArguments, + IReadOnlyList constructorParameters, + IReadOnlyList 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 IntroduceLocalSubstitute( + CodeRefactoringContext context, + TObjectCreationExpressionSyntax objectCreationExpressionSyntax, + IReadOnlyList existingArguments, + IReadOnlyList constructorParameters, + IReadOnlyList 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 IntroduceSubstitute( + CodeRefactoringContext context, + TObjectCreationExpressionSyntax objectCreationExpressionSyntax, + IReadOnlyList existingArguments, + IReadOnlyList constructorParameters, + IReadOnlyList missingArgumentsPositions, + SyntaxNode siblingNode, + Func declarationFactory) + { + var documentEditor = await DocumentEditor.CreateAsync(context.Document); + var syntaxGenerator = documentEditor.Generator; + var declarations = new List(); + var newArgumentList = new List(); + + 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().ToList(); + + if (candidateMethodSymbols.Count == 0 || candidateMethodSymbols.Count > 1) + { + return null; + } + + return candidateMethodSymbols.Single(); + } + + private IReadOnlyList GetMissingArgumentsPositions( + IReadOnlyList argumentSyntaxNodes, + IReadOnlyList parameterSymbols) + { + if (parameterSymbols.Count == 0) + { + return Array.Empty(); + } + + if (argumentSyntaxNodes.Count == 0) + { + return Enumerable.Range(0, parameterSymbols.Count).ToList(); + } + + var result = new List(); + for (var symbolPosition = 0; symbolPosition < parameterSymbols.Count; symbolPosition++) + { + if (symbolPosition >= argumentSyntaxNodes.Count || IsMissing(argumentSyntaxNodes[symbolPosition])) + { + result.Add(symbolPosition); + } + } + + return result; + } + + private int FindArgumentIndexAtSpan(IReadOnlyList argumentSyntaxNodes, TextSpan span) + { + var findArgumentIndexAtSpan = argumentSyntaxNodes.IndexOf(node => node.FullSpan.IntersectsWith(span)); + return findArgumentIndexAtSpan >= 0 ? findArgumentIndexAtSpan : 0; + } + } +} diff --git a/src/NSubstitute.Analyzers.Shared/Extensions/IEnumerableExtensions.cs b/src/NSubstitute.Analyzers.Shared/Extensions/IEnumerableExtensions.cs new file mode 100644 index 00000000..b268be21 --- /dev/null +++ b/src/NSubstitute.Analyzers.Shared/Extensions/IEnumerableExtensions.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace NSubstitute.Analyzers.Shared.Extensions +{ + internal static class IEnumerableExtensions + { + public static int IndexOf(this IEnumerable source, Func predicate) + { + var index = -1; + foreach (var item in source) + { + index++; + if (predicate(item)) + { + return index; + } + } + + return index; + } + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/Extensions/ISymbolExtensions.cs b/src/NSubstitute.Analyzers.Shared/Extensions/ISymbolExtensions.cs index 831db435..6f6de933 100644 --- a/src/NSubstitute.Analyzers.Shared/Extensions/ISymbolExtensions.cs +++ b/src/NSubstitute.Analyzers.Shared/Extensions/ISymbolExtensions.cs @@ -37,6 +37,11 @@ public static string ToMinimalMethodString(this ISymbol symbol, SemanticModel se return $"{symbol.ContainingType}.{minimumDisplayString}"; } + public static string ToMinimalSymbolString(this ISymbol symbol, SemanticModel semanticModel) + { + return symbol.ToMinimalDisplayString(semanticModel, 0, SymbolDisplayFormat.CSharpErrorMessageFormat); + } + public static bool IsLocal(this ISymbol symbol) { return symbol != null && symbol.Kind == SymbolKind.Local; diff --git a/src/NSubstitute.Analyzers.Shared/Extensions/SyntaxGeneratorExtension.cs b/src/NSubstitute.Analyzers.Shared/Extensions/SyntaxGeneratorExtension.cs new file mode 100644 index 00000000..b0360fdf --- /dev/null +++ b/src/NSubstitute.Analyzers.Shared/Extensions/SyntaxGeneratorExtension.cs @@ -0,0 +1,25 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Editing; + +namespace NSubstitute.Analyzers.Shared.Extensions +{ + internal static class SyntaxGeneratorExtension + { + public static SyntaxNode SubstituteForInvocationExpression( + this SyntaxGenerator syntaxGenerator, + IParameterSymbol parameterSymbol) + { + var qualifiedName = syntaxGenerator.QualifiedName( + syntaxGenerator.IdentifierName("NSubstitute"), + syntaxGenerator.IdentifierName("Substitute")); + + var genericName = syntaxGenerator.GenericName("For", syntaxGenerator.TypeExpression(parameterSymbol.Type)); + + var memberAccessExpression = syntaxGenerator.MemberAccessExpression(qualifiedName, genericName); + + var invocationExpression = syntaxGenerator.InvocationExpression(memberAccessExpression); + + return invocationExpression; + } + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.VisualBasic/CodeRefactoringProviders/IntroduceSubstituteCodeRefactoringProvider.cs b/src/NSubstitute.Analyzers.VisualBasic/CodeRefactoringProviders/IntroduceSubstituteCodeRefactoringProvider.cs new file mode 100644 index 00000000..8545b052 --- /dev/null +++ b/src/NSubstitute.Analyzers.VisualBasic/CodeRefactoringProviders/IntroduceSubstituteCodeRefactoringProvider.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.VisualBasic; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; +using NSubstitute.Analyzers.Shared.CodeRefactoringProviders; +using static Microsoft.CodeAnalysis.VisualBasic.SyntaxFactory; + +namespace NSubstitute.Analyzers.VisualBasic.CodeRefactoringProviders +{ + [ExportCodeRefactoringProvider(LanguageNames.VisualBasic)] + internal sealed class IntroduceSubstituteCodeRefactoringProvider : AbstractIntroduceSubstituteCodeRefactoringProvider + { + protected override IReadOnlyList GetArgumentSyntaxNodes(ArgumentListSyntax argumentListSyntax, TextSpan span) + { + return argumentListSyntax.Arguments; + } + + protected override ObjectCreationExpressionSyntax UpdateObjectCreationExpression(ObjectCreationExpressionSyntax objectCreationExpressionSyntax, IReadOnlyList argumentSyntax) + { + var updatedArgumentList = + objectCreationExpressionSyntax.ArgumentList.WithArguments(SeparatedList(argumentSyntax)); + + return objectCreationExpressionSyntax.WithArgumentList(updatedArgumentList); + } + + protected override bool IsMissing(ArgumentSyntax argumentSyntax) + { + return base.IsMissing(argumentSyntax) || argumentSyntax.IsOmitted; + } + + protected override SyntaxNode FindSiblingNodeForLocalSubstitute(ObjectCreationExpressionSyntax creationExpression) + { + var container = creationExpression.Ancestors() + .FirstOrDefault(ancestor => ancestor.Kind() == SyntaxKind.SubBlock); + + if (container is MethodBlockBaseSyntax methodBlockBaseSyntax) + { + return methodBlockBaseSyntax.Statements.FirstOrDefault(); + } + + return null; + } + + protected override SyntaxNode FindSiblingNodeForReadonlySubstitute(SyntaxNode creationExpression) + { + var typeBlockSyntax = creationExpression.Ancestors() + .OfType() + .FirstOrDefault(); + + return typeBlockSyntax?.Members.FirstOrDefault(); + } + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.CSharp/CodeRefactoringProvidersTests/CSharpCodeRefactoringProviderActionsVerifier.cs b/tests/NSubstitute.Analyzers.Tests.CSharp/CodeRefactoringProvidersTests/CSharpCodeRefactoringProviderActionsVerifier.cs new file mode 100644 index 00000000..cd6894d2 --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.CSharp/CodeRefactoringProvidersTests/CSharpCodeRefactoringProviderActionsVerifier.cs @@ -0,0 +1,12 @@ +using NSubstitute.Analyzers.Tests.Shared.CodeRefactoringProviders; + +namespace NSubstitute.Analyzers.Tests.CSharp.CodeRefactoringProvidersTests +{ + public abstract class CSharpCodeRefactoringProviderActionsVerifier : CodeRefactoringProviderActionsVerifier + { + protected CSharpCodeRefactoringProviderActionsVerifier() + : base(CSharpWorkspaceFactory.Default) + { + } + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.CSharp/CodeRefactoringProvidersTests/CSharpCodeRefactoringProviderVerifier.cs b/tests/NSubstitute.Analyzers.Tests.CSharp/CodeRefactoringProvidersTests/CSharpCodeRefactoringProviderVerifier.cs new file mode 100644 index 00000000..75e50c15 --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.CSharp/CodeRefactoringProvidersTests/CSharpCodeRefactoringProviderVerifier.cs @@ -0,0 +1,12 @@ +using NSubstitute.Analyzers.Tests.Shared.CodeRefactoringProviders; + +namespace NSubstitute.Analyzers.Tests.CSharp.CodeRefactoringProvidersTests +{ + public abstract class CSharpCodeRefactoringProviderVerifier : CodeRefactoringProviderVerifier + { + protected CSharpCodeRefactoringProviderVerifier() + : base(CSharpWorkspaceFactory.Default) + { + } + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.CSharp/CodeRefactoringProvidersTests/IntroduceSubstituteRefactoringProviderTests/IntroduceSubstituteCodeRefactoringActionsTests.cs b/tests/NSubstitute.Analyzers.Tests.CSharp/CodeRefactoringProvidersTests/IntroduceSubstituteRefactoringProviderTests/IntroduceSubstituteCodeRefactoringActionsTests.cs new file mode 100644 index 00000000..ae2eaeda --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.CSharp/CodeRefactoringProvidersTests/IntroduceSubstituteRefactoringProviderTests/IntroduceSubstituteCodeRefactoringActionsTests.cs @@ -0,0 +1,222 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeRefactorings; +using NSubstitute.Analyzers.CSharp.CodeRefactoringProviders; +using NSubstitute.Analyzers.Tests.Shared.CodeRefactoringProviders; +using Xunit; + +namespace NSubstitute.Analyzers.Tests.CSharp.CodeRefactoringProvidersTests.IntroduceSubstituteRefactoringProviderTests +{ + public class IntroduceSubstituteCodeRefactoringActionsTests : CSharpCodeRefactoringProviderActionsVerifier, IIntroduceSubstituteCodeRefactoringActionsVerifier + { + private static readonly string IntroduceReadonlySubstitutesTitle = "Introduce readonly substitutes for missing arguments"; + private static readonly string IntroduceLocalSubstitutesTitle = "Introduce local substitutes for missing arguments"; + private static readonly string IntroduceReadonlySubstituteForService = "Introduce readonly substitute for MyNamespace.IService"; + + protected override CodeRefactoringProvider CodeRefactoringProvider { get; } = new IntroduceSubstituteCodeRefactoringProvider(); + + [Fact] + public async Task DoesNotCreateCodeActions_WhenConstructorHasNoParameters() + { + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public class Foo + { + public Foo() + { + } + } + + public class FooTests + { + public void Test() + { + var target = new Foo([||]); + } + } +}"; + + await VerifyCodeActions(source, Array.Empty()); + } + + [Fact] + public async Task DoesNotCreateCodeActions_WhenMultipleCandidateConstructorsAvailable() + { + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public interface IService {} + public interface IOtherService {} + public class Foo + { + public Foo(IService service) { } + public Foo(IOtherService otherService) { } + } + + public class FooTests + { + public void Test() + { + var target = new Foo([||]); + } + } +}"; + + await VerifyCodeActions(source, Array.Empty()); + } + + [Fact] + public async Task DoesNotCreateCodeActions_WhenAllParametersProvided() + { + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public interface IService {} + public class Foo + { + public Foo(IService service) { } + } + + public class FooTests + { + public void Test() + { + var service = Substitute.For(); + var target = new Foo([||]service); + } + } +}"; + await VerifyCodeActions(source, Array.Empty()); + } + + [Fact] + public async Task DoesNotCreateCodeActionsForIntroducingSpecificSubstitute_WhenArgumentAtPositionProvided() + { + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public interface IService {} + public interface IOtherService {} + public class Foo + { + public Foo(IService service, IOtherService otherService) { } + } + + public class FooTests + { + public void Test() + { + var service = Substitute.For(); + var target = new Foo(service[||],); + } + } +}"; + await VerifyCodeActions(source, IntroduceLocalSubstitutesTitle, IntroduceReadonlySubstitutesTitle); + } + + [Fact] + public async Task DoesNotCreateCodeActionsForIntroducingLocalSubstitute_WhenLocalVariableCannotBeIntroduced() + { + const int refactoringCount = 3; + var singleRefactoringActions = new[] + { + IntroduceReadonlySubstituteForService, + IntroduceReadonlySubstitutesTitle + }; + + var allRefactoringsActions = + Enumerable.Range(0, refactoringCount).SelectMany(_ => singleRefactoringActions).ToArray(); + + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public interface IService {} + public interface IOtherService {} + public class Foo + { + public Foo(IService service, IOtherService otherService) { } + } + + public class FooTests + { + private readonly Foo foo = new Foo([||]) + public FooTests() => new Foo([||]) + public void Test() => new Foo([||]) + } +}"; + await VerifyCodeActions(source, allRefactoringsActions); + } + + [Theory] + [InlineData("new Foo([||],);", new[] { "Introduce local substitute for MyNamespace.IService", "Introduce readonly substitute for MyNamespace.IService" })] + [InlineData("new Foo(,[||]);", new[] { "Introduce local substitute for MyNamespace.IOtherService", "Introduce readonly substitute for MyNamespace.IOtherService" })] + public async Task CreatesCodeActionsForIntroducingSpecificSubstitute_WhenArgumentAtPositionNotProvided(string creationExpression, string[] expectedSpecificSubstituteActions) + { + var expectedAllActions = new[] + { + IntroduceLocalSubstitutesTitle, + IntroduceReadonlySubstitutesTitle + }.Concat(expectedSpecificSubstituteActions).ToArray(); + + var source = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface IService {{}} + public interface IOtherService {{}} + public class Foo + {{ + public Foo(IService service, IOtherService otherService) {{ }} + }} + + public class FooTests + {{ + public void Test() + {{ + var target = {creationExpression} + }} + }} +}}"; + await VerifyCodeActions(source, expectedAllActions); + } + + [Fact] + public async Task DoesNotCreateCodeActions_WhenSymbolIsNotConstructorInvocation() + { + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public interface IService {} + public interface IOtherService {} + public class Foo + { + public Foo([||]IService service, [||]IIOtherService otherService) { } + public Baz(int x, [||]) { } + } + + public class FooTests + { + public void Test() + { + var service = Substitute.For([||]); + var x = FooBar(1, [||]); + var z = new int[] { [||] }; + } + + private void FooBar([||]int x,[||]int y) + { + } + } +}"; + await VerifyCodeActions(source); + } + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.CSharp/CodeRefactoringProvidersTests/IntroduceSubstituteRefactoringProviderTests/IntroduceSubstituteCodeRefactoringProviderTests.cs b/tests/NSubstitute.Analyzers.Tests.CSharp/CodeRefactoringProvidersTests/IntroduceSubstituteRefactoringProviderTests/IntroduceSubstituteCodeRefactoringProviderTests.cs new file mode 100644 index 00000000..0492c0d6 --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.CSharp/CodeRefactoringProvidersTests/IntroduceSubstituteRefactoringProviderTests/IntroduceSubstituteCodeRefactoringProviderTests.cs @@ -0,0 +1,511 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeRefactorings; +using NSubstitute.Analyzers.CSharp.CodeRefactoringProviders; +using NSubstitute.Analyzers.Tests.Shared.CodeRefactoringProviders; +using Xunit; + +namespace NSubstitute.Analyzers.Tests.CSharp.CodeRefactoringProvidersTests.IntroduceSubstituteRefactoringProviderTests +{ + public class IntroduceSubstituteCodeRefactoringProviderTests : CSharpCodeRefactoringProviderVerifier, IIntroduceSubstituteCodeRefactoringProviderVerifier + { + public static IEnumerable AllArgumentsMissingTestCases + { + get + { + yield return new object[] { "new Foo([||]);", "new Foo(service, otherService);" }; + yield return new object[] { "new Foo([||])", "new Foo(service, otherService)" }; + yield return new object[] { "new Foo(, [||]);", "new Foo(service, otherService);" }; + yield return new object[] { "new Foo([||],);", "new Foo(service, otherService);" }; + yield return new object[] { "new Foo([||]", "new Foo(service, otherService" }; + yield return new object[] { "new Foo(, [||]", "new Foo(service, otherService" }; + yield return new object[] { "new Foo([||],", "new Foo(service, otherService" }; + } + } + + public static IEnumerable SomeArgumentMissingTestCases + { + get + { + yield return new object[] + { + "new Foo([||],someOtherService, someAnotherService, );", + "new Foo(service, someOtherService, someAnotherService, yetAnotherService);" + }; + yield return new object[] + { + "new Foo([||],someOtherService, someAnotherService, )", + "new Foo(service, someOtherService, someAnotherService, yetAnotherService)" + }; + yield return new object[] + { + "new Foo(,someOtherService, someAnotherService, [||]);", + "new Foo(service, someOtherService, someAnotherService, yetAnotherService);" + }; + yield return new object[] + { + "new Foo([||],someOtherService, someAnotherService,", + "new Foo(service, someOtherService, someAnotherService, yetAnotherService" + }; + yield return new object[] + { + "new Foo(,someOtherService, someAnotherService, [||]", + "new Foo(service, someOtherService, someAnotherService, yetAnotherService" + }; + } + } + + public static IEnumerable SpecificArgumentMissingTestCases + { + get + { + yield return new object[] + { + "new Foo(service, [||],, someYetAnotherService);", + "new Foo(service, otherService,, someYetAnotherService);" + }; + + yield return new object[] + { + "new Foo(service, [||],, someYetAnotherService)", + "new Foo(service, otherService,, someYetAnotherService)" + }; + } + } + + protected override CodeRefactoringProvider CodeRefactoringProvider { get; } = new IntroduceSubstituteCodeRefactoringProvider(); + + [Theory] + [MemberData(nameof(AllArgumentsMissingTestCases))] + public async Task GeneratesConstructorArgumentListWithLocalVariables_WhenAllArgumentsMissing(string creationExpression, string expectedCreationExpression) + { + var source = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface IService {{ }} + public interface IOtherService {{ }} + public class Foo + {{ + public Foo(IService service, IOtherService otherService) + {{ + }} + }} + + public class FooTests + {{ + public void Test() + {{ + var target = {creationExpression} + }} + }} +}}"; + var newSource = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface IService {{ }} + public interface IOtherService {{ }} + public class Foo + {{ + public Foo(IService service, IOtherService otherService) + {{ + }} + }} + + public class FooTests + {{ + public void Test() + {{ + var service = Substitute.For(); + var otherService = Substitute.For(); + var target = {expectedCreationExpression} + }} + }} +}}"; + await VerifyRefactoring(source, newSource, refactoringIndex: 2); + } + + [Fact] + public async Task GeneratesConstructorArgumentListForAllArguments_WithFullyQualifiedName_WhenNamespaceNotImported() + { + var source = @" +namespace MyNamespace +{ + public interface IService { } + public interface IOtherService { } + public class Foo + { + public Foo(IService service, IOtherService otherService) + { + } + } + + public class FooTests + { + public void Test() + { + var target = new Foo([||]); + } + } +}"; + var newSource = @" +namespace MyNamespace +{ + public interface IService { } + public interface IOtherService { } + public class Foo + { + public Foo(IService service, IOtherService otherService) + { + } + } + + public class FooTests + { + public void Test() + { + var service = NSubstitute.Substitute.For(); + var otherService = NSubstitute.Substitute.For(); + var target = new Foo(service, otherService); + } + } +}"; + await VerifyRefactoring(source, newSource, refactoringIndex: 2); + } + + [Fact] + public async Task GeneratesConstructorArgumentListForSpecificSubstitute_WithFullyQualifiedName_WhenNamespaceNotImported() + { + var source = @" +namespace MyNamespace +{ + public interface IService { } + public interface IOtherService { } + public class Foo + { + public Foo(IService service, IOtherService otherService) + { + } + } + + public class FooTests + { + public void Test() + { + var target = new Foo([||],); + } + } +}"; + var newSource = @" +namespace MyNamespace +{ + public interface IService { } + public interface IOtherService { } + public class Foo + { + public Foo(IService service, IOtherService otherService) + { + } + } + + public class FooTests + { + public void Test() + { + var service = NSubstitute.Substitute.For(); + var target = new Foo(service,); + } + } +}"; + await VerifyRefactoring(source, newSource, refactoringIndex: 0); + } + + [Theory] + [MemberData(nameof(SomeArgumentMissingTestCases))] + public async Task GeneratesConstructorArgumentListWithLocalVariables_WhenSomeArgumentsMissing(string creationExpression, string expectedCreationExpression) + { + var source = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface IService {{ }} + public interface IOtherService {{ }} + public interface IAnotherService {{ }} + public interface IYetAnotherService {{ }} + public class Foo + {{ + public Foo(IService service, IOtherService otherService, IAnotherService anotherService, IYetAnotherService yetAnotherService) + {{ + }} + }} + + public class FooTests + {{ + public void Test() + {{ + var someOtherService = Substitute.For(); + var someAnotherService = Substitute.For(); + var target = {creationExpression} + }} + }} +}}"; + var newSource = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface IService {{ }} + public interface IOtherService {{ }} + public interface IAnotherService {{ }} + public interface IYetAnotherService {{ }} + public class Foo + {{ + public Foo(IService service, IOtherService otherService, IAnotherService anotherService, IYetAnotherService yetAnotherService) + {{ + }} + }} + + public class FooTests + {{ + public void Test() + {{ + var service = Substitute.For(); + var yetAnotherService = Substitute.For(); + var someOtherService = Substitute.For(); + var someAnotherService = Substitute.For(); + var target = {expectedCreationExpression} + }} + }} +}}"; + await VerifyRefactoring(source, newSource, refactoringIndex: 2); + } + + [Theory] + [MemberData(nameof(AllArgumentsMissingTestCases))] + public async Task GeneratesConstructorArgumentListWithReadonlyFields_WhenAllArgumentsMissing(string creationExpression, string expectedCreationExpression) + { + var source = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface IService {{ }} + public interface IOtherService {{ }} + public class Foo + {{ + public Foo(IService service, IOtherService otherService) + {{ + }} + }} + + public class FooTests + {{ + public void Test() + {{ + var target = {creationExpression} + }} + }} +}}"; + var newSource = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface IService {{ }} + public interface IOtherService {{ }} + public class Foo + {{ + public Foo(IService service, IOtherService otherService) + {{ + }} + }} + + public class FooTests + {{ + private readonly IService service = Substitute.For(); + private readonly IOtherService otherService = Substitute.For(); + + public void Test() + {{ + var target = {expectedCreationExpression} + }} + }} +}}"; + await VerifyRefactoring(source, newSource, refactoringIndex: 3); + } + + [Theory] + [MemberData(nameof(SomeArgumentMissingTestCases))] + public async Task GeneratesConstructorArgumentListWithReadonlyFields_WhenSomeArgumentsMissing(string creationExpression, string expectedCreationExpression) + { + var source = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface IService {{ }} + public interface IOtherService {{ }} + public interface IAnotherService {{ }} + public interface IYetAnotherService {{ }} + public class Foo + {{ + public Foo(IService service, IOtherService otherService, IAnotherService anotherService, IYetAnotherService yetAnotherService) + {{ + }} + }} + + public class FooTests + {{ + public void Test() + {{ + var someOtherService = Substitute.For(); + var someAnotherService = Substitute.For(); + var target = {creationExpression} + }} + }} +}}"; + var newSource = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface IService {{ }} + public interface IOtherService {{ }} + public interface IAnotherService {{ }} + public interface IYetAnotherService {{ }} + public class Foo + {{ + public Foo(IService service, IOtherService otherService, IAnotherService anotherService, IYetAnotherService yetAnotherService) + {{ + }} + }} + + public class FooTests + {{ + private readonly IService service = Substitute.For(); + private readonly IYetAnotherService yetAnotherService = Substitute.For(); + + public void Test() + {{ + var someOtherService = Substitute.For(); + var someAnotherService = Substitute.For(); + var target = {expectedCreationExpression} + }} + }} +}}"; + await VerifyRefactoring(source, newSource, refactoringIndex: 3); + } + + [Theory] + [MemberData(nameof(SpecificArgumentMissingTestCases))] + public async Task GeneratesConstructorArgumentListWithLocalVariable_ForSpecificArgument_WhenArgumentMissing(string creationExpression, string expectedCreationExpression) + { + var source = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface IService {{ }} + public interface IOtherService {{ }} + public interface IAnotherService {{ }} + public interface IYetAnotherService {{ }} + public class Foo + {{ + public Foo(IService service, IOtherService otherService, IAnotherService anotherService, IYetAnotherService yetAnotherService) + {{ + }} + }} + + public class FooTests + {{ + public void Test() + {{ + var service = Substitute.For(); + var someYetAnotherService = Substitute.For(); + var target = {creationExpression} + }} + }} +}}"; + var newSource = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface IService {{ }} + public interface IOtherService {{ }} + public interface IAnotherService {{ }} + public interface IYetAnotherService {{ }} + public class Foo + {{ + public Foo(IService service, IOtherService otherService, IAnotherService anotherService, IYetAnotherService yetAnotherService) + {{ + }} + }} + + public class FooTests + {{ + public void Test() + {{ + var otherService = Substitute.For(); + var service = Substitute.For(); + var someYetAnotherService = Substitute.For(); + var target = {expectedCreationExpression} + }} + }} +}}"; + await VerifyRefactoring(source, newSource, refactoringIndex: 0); + } + + [Theory] + [MemberData(nameof(SpecificArgumentMissingTestCases))] + public async Task GeneratesConstructorArgumentListWithReadonlyFields_ForSpecificArgument_WhenArgumentMissing(string creationExpression, string expectedCreationExpression) + { + var source = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface IService {{ }} + public interface IOtherService {{ }} + public interface IAnotherService {{ }} + public interface IYetAnotherService {{ }} + public class Foo + {{ + public Foo(IService service, IOtherService otherService, IAnotherService anotherService, IYetAnotherService yetAnotherService) + {{ + }} + }} + + public class FooTests + {{ + private readonly IService service = Substitute.For(); + private readonly IYetAnotherService someYetAnotherService = Substitute.For(); + public void Test() + {{ + var target = {creationExpression} + }} + }} +}}"; + var newSource = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface IService {{ }} + public interface IOtherService {{ }} + public interface IAnotherService {{ }} + public interface IYetAnotherService {{ }} + public class Foo + {{ + public Foo(IService service, IOtherService otherService, IAnotherService anotherService, IYetAnotherService yetAnotherService) + {{ + }} + }} + + public class FooTests + {{ + private readonly IOtherService otherService = Substitute.For(); + private readonly IService service = Substitute.For(); + private readonly IYetAnotherService someYetAnotherService = Substitute.For(); + public void Test() + {{ + var target = {expectedCreationExpression} + }} + }} +}}"; + await VerifyRefactoring(source, newSource, refactoringIndex: 1); + } + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.CSharp/ConventionTests/AnalyzersConventionTests.cs b/tests/NSubstitute.Analyzers.Tests.CSharp/ConventionTests/AnalyzersConventionTests.cs index fe6ba0c8..197b7f63 100644 --- a/tests/NSubstitute.Analyzers.Tests.CSharp/ConventionTests/AnalyzersConventionTests.cs +++ b/tests/NSubstitute.Analyzers.Tests.CSharp/ConventionTests/AnalyzersConventionTests.cs @@ -37,5 +37,17 @@ public void CodeFixProvidersInheritanceHierarchyShouldBeSatisfied() { _fixture.AssertCodeFixProviderInheritanceFromAssemblyContaining(); } + + [Fact] + public void CodeRefactoringProvidersAttributeConventionsShouldBeSatisfied() + { + _fixture.AssertExportCodeRefactoringProviderAttributeUsageFromAssemblyContaining(LanguageNames.CSharp); + } + + [Fact] + public void CodeRefactoringProvidersInheritanceHierarchyShouldBeSatisfied() + { + _fixture.AssertCodeRefactoringProviderInheritanceFromAssemblyContaining(); + } } } \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.Shared/CodeRefactoringProviders/CodeRefactoringProviderActionsVerifier.cs b/tests/NSubstitute.Analyzers.Tests.Shared/CodeRefactoringProviders/CodeRefactoringProviderActionsVerifier.cs new file mode 100644 index 00000000..7a30857a --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.Shared/CodeRefactoringProviders/CodeRefactoringProviderActionsVerifier.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.Text; + +namespace NSubstitute.Analyzers.Tests.Shared.CodeRefactoringProviders +{ + public abstract class CodeRefactoringProviderActionsVerifier : CodeVerifier + { + protected CodeRefactoringProviderActionsVerifier(WorkspaceFactory workspaceFactory) + : base(workspaceFactory) + { + } + + protected abstract CodeRefactoringProvider CodeRefactoringProvider { get; } + + protected async Task VerifyCodeActions(string source, params string[] expectedCodeActionTitles) + { + var parserResult = TextParser.GetSpans(source); + var spans = parserResult.Spans.Select(position => position.Span).ToList(); + + if (spans.Count == 0) + { + throw new ArgumentException("Refactoring spans should not be empty", nameof(source)); + } + + using (var workspace = new AdhocWorkspace()) + { + var project = AddProject(workspace.CurrentSolution, parserResult.Text); + var document = project.Documents.Single(); + + var codeActionTasks = spans + .Select(span => RegisterCodeRefactoringActions(document, span)).ToList(); + + await Task.WhenAll(codeActionTasks); + + var codeActions = codeActionTasks.SelectMany(task => task.Result).ToList(); + + codeActions.Should().NotBeNull(); + codeActions.Select(action => action.Title).Should().BeEquivalentTo(expectedCodeActionTitles ?? Array.Empty()); + } + } + + private async Task> RegisterCodeRefactoringActions(Document document, TextSpan span) + { + var builder = ImmutableArray.CreateBuilder(); + var context = new CodeRefactoringContext(document, span, a => builder.Add(a), CancellationToken.None); + await CodeRefactoringProvider.ComputeRefactoringsAsync(context); + + return builder.ToImmutable(); + } + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.Shared/CodeRefactoringProviders/CodeRefactoringProviderVerifier.cs b/tests/NSubstitute.Analyzers.Tests.Shared/CodeRefactoringProviders/CodeRefactoringProviderVerifier.cs new file mode 100644 index 00000000..781a6651 --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.Shared/CodeRefactoringProviders/CodeRefactoringProviderVerifier.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.Text; +using NSubstitute.Analyzers.Tests.Shared.Extensions; + +namespace NSubstitute.Analyzers.Tests.Shared.CodeRefactoringProviders +{ + public abstract class CodeRefactoringProviderVerifier : CodeVerifier + { + protected CodeRefactoringProviderVerifier(WorkspaceFactory workspaceFactory) + : base(workspaceFactory) + { + } + + protected abstract CodeRefactoringProvider CodeRefactoringProvider { get; } + + protected async Task VerifyRefactoring(string oldSource, string newSource, int? refactoringIndex = null) + { + var parserResult = TextParser.GetSpans(oldSource); + var spans = parserResult.Spans; + if (spans.Any() == false) + { + throw new ArgumentException("Refactoring spans should not be empty", nameof(oldSource)); + } + + using (var workspace = new AdhocWorkspace()) + { + var project = AddProject(workspace.CurrentSolution, parserResult.Text); + var document = project.Documents.Single(); + + var actions = await RegisterCodeRefactoringActions(document, spans.Single().Span); + + var codeAction = actions[refactoringIndex ?? 0]; + var updatedDocument = await document.ApplyCodeAction(codeAction); + var updatedSource = await updatedDocument.ToFullString(); + updatedSource.Should().Be(newSource); + } + } + + private async Task> RegisterCodeRefactoringActions(Document document, TextSpan span) + { + var builder = ImmutableArray.CreateBuilder(); + var context = new CodeRefactoringContext(document, span, a => builder.Add(a), CancellationToken.None); + await CodeRefactoringProvider.ComputeRefactoringsAsync(context); + + return builder.ToImmutable(); + } + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.Shared/CodeRefactoringProviders/IIntroduceSubstituteCodeRefactoringActionsVerifier.cs b/tests/NSubstitute.Analyzers.Tests.Shared/CodeRefactoringProviders/IIntroduceSubstituteCodeRefactoringActionsVerifier.cs new file mode 100644 index 00000000..735f09a1 --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.Shared/CodeRefactoringProviders/IIntroduceSubstituteCodeRefactoringActionsVerifier.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; + +namespace NSubstitute.Analyzers.Tests.Shared.CodeRefactoringProviders +{ + public interface IIntroduceSubstituteCodeRefactoringActionsVerifier + { + Task DoesNotCreateCodeActions_WhenConstructorHasNoParameters(); + + Task DoesNotCreateCodeActions_WhenMultipleCandidateConstructorsAvailable(); + + Task DoesNotCreateCodeActions_WhenAllParametersProvided(); + + Task DoesNotCreateCodeActionsForIntroducingSpecificSubstitute_WhenArgumentAtPositionProvided(); + + Task DoesNotCreateCodeActionsForIntroducingLocalSubstitute_WhenLocalVariableCannotBeIntroduced(); + + Task CreatesCodeActionsForIntroducingSpecificSubstitute_WhenArgumentAtPositionNotProvided(string creationExpression, string[] expectedSpecificSubstituteActions); + + Task DoesNotCreateCodeActions_WhenSymbolIsNotConstructorInvocation(); + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.Shared/CodeRefactoringProviders/IIntroduceSubstituteCodeRefactoringProviderVerifier.cs b/tests/NSubstitute.Analyzers.Tests.Shared/CodeRefactoringProviders/IIntroduceSubstituteCodeRefactoringProviderVerifier.cs new file mode 100644 index 00000000..5c835eec --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.Shared/CodeRefactoringProviders/IIntroduceSubstituteCodeRefactoringProviderVerifier.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; + +namespace NSubstitute.Analyzers.Tests.Shared.CodeRefactoringProviders +{ + public interface IIntroduceSubstituteCodeRefactoringProviderVerifier + { + Task GeneratesConstructorArgumentListWithLocalVariables_WhenAllArgumentsMissing(string creationExpression, string expectedCreationExpression); + + Task GeneratesConstructorArgumentListForAllArguments_WithFullyQualifiedName_WhenNamespaceNotImported(); + + Task GeneratesConstructorArgumentListForSpecificSubstitute_WithFullyQualifiedName_WhenNamespaceNotImported(); + + Task GeneratesConstructorArgumentListWithLocalVariables_WhenSomeArgumentsMissing(string creationExpression, string expectedCreationExpression); + + Task GeneratesConstructorArgumentListWithReadonlyFields_WhenAllArgumentsMissing(string creationExpression, string expectedCreationExpression); + + Task GeneratesConstructorArgumentListWithReadonlyFields_WhenSomeArgumentsMissing(string creationExpression, string expectedCreationExpression); + + Task GeneratesConstructorArgumentListWithLocalVariable_ForSpecificArgument_WhenArgumentMissing(string creationExpression, string expectedCreationExpression); + + Task GeneratesConstructorArgumentListWithReadonlyFields_ForSpecificArgument_WhenArgumentMissing(string creationExpression, string expectedCreationExpression); + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.Shared/Fixtures/AnalyzersConventionFixture.cs b/tests/NSubstitute.Analyzers.Tests.Shared/Fixtures/AnalyzersConventionFixture.cs index d2eaf1ed..61b6aae8 100644 --- a/tests/NSubstitute.Analyzers.Tests.Shared/Fixtures/AnalyzersConventionFixture.cs +++ b/tests/NSubstitute.Analyzers.Tests.Shared/Fixtures/AnalyzersConventionFixture.cs @@ -4,6 +4,7 @@ using System.Reflection; using FluentAssertions; using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CodeRefactorings; using Microsoft.CodeAnalysis.Diagnostics; using NSubstitute.Analyzers.Shared.DiagnosticAnalyzers; using NSubstitute.Analyzers.Tests.Shared.Extensions; @@ -70,6 +71,33 @@ public void AssertDiagnosticAnalyzerAttributeUsageFromAssemblyContaining(Type ty $"because each analyzer should support only selected language ${expectedLanguage}"); } + public void AssertCodeRefactoringProviderInheritanceFromAssemblyContaining() + { + AssertCodeRefactoringProviderInheritanceFromAssemblyContaining(typeof(T)); + } + + public void AssertCodeRefactoringProviderInheritanceFromAssemblyContaining(Type type) + { + var codeRefactoringProviders = GetCodeRefactoringProviders(type.Assembly); + codeRefactoringProviders.Should().OnlyContain(analyzer => analyzer.IsSealed); + } + + public void AssertExportCodeRefactoringProviderAttributeUsageFromAssemblyContaining(string expectedLanguage) + { + AssertExportCodeRefactoringProviderAttributeUsageFromAssemblyContaining(typeof(T), expectedLanguage); + } + + public void AssertExportCodeRefactoringProviderAttributeUsageFromAssemblyContaining(Type type, string expectedLanguage) + { + var codeRefactoringProviders = GetCodeRefactoringProviders(type.Assembly).ToList(); + + codeRefactoringProviders.Should().OnlyContain(innerType => innerType.GetCustomAttributes(false).Count() == 1, "because each code refactoring provider should be marked with only one attribute ExportCodeRefactoringProviderAttribute"); + codeRefactoringProviders.SelectMany(innerType => innerType.GetCustomAttributes(false)).Should() + .OnlyContain( + attr => attr.Languages.Length == 1 && attr.Languages.Count(lang => lang == expectedLanguage) == 1, + $"because each code refactoring provider should support only selected language ${expectedLanguage}"); + } + private IEnumerable GetDiagnosticAnalyzers(Assembly assembly) { return assembly.GetTypesAssignableTo(); @@ -79,5 +107,10 @@ private IEnumerable GetCodeFixProviders(Assembly assembly) { return assembly.GetTypesAssignableTo(); } + + private IEnumerable GetCodeRefactoringProviders(Assembly assembly) + { + return assembly.GetTypesAssignableTo(); + } } } \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.VisualBasic/CodeRefactoringProvidersTests/IntroduceSubstituteRefactoringProviderTests/IntroduceSubstituteCodeRefactoringActionsTests.cs b/tests/NSubstitute.Analyzers.Tests.VisualBasic/CodeRefactoringProvidersTests/IntroduceSubstituteRefactoringProviderTests/IntroduceSubstituteCodeRefactoringActionsTests.cs new file mode 100644 index 00000000..e0088476 --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.VisualBasic/CodeRefactoringProvidersTests/IntroduceSubstituteRefactoringProviderTests/IntroduceSubstituteCodeRefactoringActionsTests.cs @@ -0,0 +1,219 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeRefactorings; +using NSubstitute.Analyzers.Tests.Shared.CodeRefactoringProviders; +using NSubstitute.Analyzers.VisualBasic.CodeRefactoringProviders; +using Xunit; + +namespace NSubstitute.Analyzers.Tests.VisualBasic.CodeRefactoringProvidersTests.IntroduceSubstituteRefactoringProviderTests +{ + public class IntroduceSubstituteCodeRefactoringActionsTests : VisualBasicCodeRefactoringProviderActionsVerifier, IIntroduceSubstituteCodeRefactoringActionsVerifier + { + private static readonly string IntroduceReadonlySubstitutesTitle = "Introduce readonly substitutes for missing arguments"; + private static readonly string IntroduceLocalSubstitutesTitle = "Introduce local substitutes for missing arguments"; + private static readonly string IntroduceReadonlySubstituteForService = "Introduce readonly substitute for MyNamespace.IService"; + + protected override CodeRefactoringProvider CodeRefactoringProvider { get; } = new IntroduceSubstituteCodeRefactoringProvider(); + + [Fact] + public async Task DoesNotCreateCodeActions_WhenConstructorHasNoParameters() + { + var source = @"Imports NSubstitute + +Namespace MyNamespace + Public Class Foo + Public Sub New() + End Sub + End Class + + Public Class FooTests + Public Sub Test() + Dim target = New Foo([||]) + End Sub + End Class +End Namespace"; + + await VerifyCodeActions(source, Array.Empty()); + } + + [Fact] + public async Task DoesNotCreateCodeActions_WhenMultipleCandidateConstructorsAvailable() + { + var source = @"Imports NSubstitute + +Namespace MyNamespace + Interface IService + End Interface + + Interface IOtherService + End Interface + + Public Class Foo + Public Sub New(ByVal service As IService) + End Sub + + Public Sub New(ByVal otherService As IOtherService) + End Sub + End Class + + Public Class FooTests + Public Sub Test() + Dim target = New Foo([||]) + End Sub + End Class +End Namespace"; + + await VerifyCodeActions(source, Array.Empty()); + } + + [Fact] + public async Task DoesNotCreateCodeActions_WhenAllParametersProvided() + { + var source = @"Imports NSubstitute + +Namespace MyNamespace + Interface IService + End Interface + + Public Class Foo + Public Sub New(ByVal service As IService) + End Sub + End Class + + Public Class FooTests + Public Sub Test() + Dim service = Substitute.[For](Of IService)() + Dim target = New Foo([||]service) + service + End Sub + End Class +End Namespace"; + + await VerifyCodeActions(source, Array.Empty()); + } + + [Fact] + public async Task DoesNotCreateCodeActionsForIntroducingSpecificSubstitute_WhenArgumentAtPositionProvided() + { + var source = @"Imports NSubstitute + +Namespace MyNamespace + Interface IService + End Interface + + Interface IOtherService + End Interface + + Public Class Foo + Public Sub New(ByVal service As IService, ByVal otherService As IOtherService) + End Sub + End Class + + Public Class FooTests + Public Sub Test() + Dim service = Substitute.[For](Of IService)() + Dim target = New Foo(service[||],) + End Sub + End Class +End Namespace"; + + await VerifyCodeActions(source, IntroduceLocalSubstitutesTitle, IntroduceReadonlySubstitutesTitle); + } + + [Fact] + public async Task DoesNotCreateCodeActionsForIntroducingLocalSubstitute_WhenLocalVariableCannotBeIntroduced() + { + var source = @"Imports NSubstitute + +Namespace MyNamespace + Interface IService + End Interface + + Interface IOtherService + End Interface + + Public Class Foo + Public Sub New(ByVal service As IService, ByVal otherService As IOtherService) + End Sub + End Class + + Public Class FooTests + Private ReadOnly foo As Foo = New Foo([||]) + End Class +End Namespace"; + + await VerifyCodeActions(source, IntroduceReadonlySubstituteForService, IntroduceReadonlySubstitutesTitle); + } + + [Theory] + [InlineData("New Foo([||],)", new[] { "Introduce local substitute for MyNamespace.IService", "Introduce readonly substitute for MyNamespace.IService" })] + [InlineData("New Foo(,[||])", new[] { "Introduce local substitute for MyNamespace.IOtherService", "Introduce readonly substitute for MyNamespace.IOtherService" })] + public async Task CreatesCodeActionsForIntroducingSpecificSubstitute_WhenArgumentAtPositionNotProvided(string creationExpression, string[] expectedSpecificSubstituteActions) + { + var expectedAllActions = new[] + { + IntroduceLocalSubstitutesTitle, + IntroduceReadonlySubstitutesTitle + }.Concat(expectedSpecificSubstituteActions).ToArray(); + + var source = $@"Imports NSubstitute + +Namespace MyNamespace + Interface IService + End Interface + + Interface IOtherService + End Interface + + Public Class Foo + Public Sub New(ByVal service As IService, ByVal otherService As IOtherService) + End Sub + End Class + + Public Class FooTests + Public Sub Test() + Dim target = {creationExpression} + End Sub + End Class +End Namespace"; + + await VerifyCodeActions(source, expectedAllActions); + } + + [Fact] + public async Task DoesNotCreateCodeActions_WhenSymbolIsNotConstructorInvocation() + { + var source = @"Imports NSubstitute + +Namespace MyNamespace + Interface IService + End Interface + + Interface IOtherService + End Interface + + Public Class Foo + Public Sub New([||]ByVal service As IService, [||]ByVal otherService As IIOtherService) + End Sub + + Public Sub New(ByVal x As Integer, [||]) + End Sub + End Class + + Public Class FooTests + Public Sub Test() + Dim service = Substitute.[For](Of IService)([||]) + Dim x = FooBar(1, [||]) + Dim z = New Integer() {[||]} + End Sub + + Private Sub FooBar([||]ByVal x As Integer, [||]ByVal y As Integer) + End Sub + End Class +End Namespace"; + + await VerifyCodeActions(source, Array.Empty()); + } + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.VisualBasic/CodeRefactoringProvidersTests/IntroduceSubstituteRefactoringProviderTests/IntroduceSubstituteCodeRefactoringProviderTests.cs b/tests/NSubstitute.Analyzers.Tests.VisualBasic/CodeRefactoringProvidersTests/IntroduceSubstituteRefactoringProviderTests/IntroduceSubstituteCodeRefactoringProviderTests.cs new file mode 100644 index 00000000..402f3b2b --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.VisualBasic/CodeRefactoringProvidersTests/IntroduceSubstituteRefactoringProviderTests/IntroduceSubstituteCodeRefactoringProviderTests.cs @@ -0,0 +1,542 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeRefactorings; +using NSubstitute.Analyzers.Tests.Shared.CodeRefactoringProviders; +using NSubstitute.Analyzers.VisualBasic.CodeRefactoringProviders; +using Xunit; + +namespace NSubstitute.Analyzers.Tests.VisualBasic.CodeRefactoringProvidersTests.IntroduceSubstituteRefactoringProviderTests +{ + public class IntroduceSubstituteCodeRefactoringProviderTests : VisualBasicCodeRefactoringProviderVerifier, IIntroduceSubstituteCodeRefactoringProviderVerifier + { + public static IEnumerable AllArgumentsMissingTestCases + { + get + { + yield return new object[] { "New Foo([||])", "New Foo(service, otherService)" }; + yield return new object[] { "New Foo(, [||])", "New Foo(service, otherService)" }; + yield return new object[] { "New Foo([||],)", "New Foo(service, otherService)" }; + yield return new object[] { "New Foo([||]", "New Foo(service, otherService" }; + yield return new object[] { "New Foo(, [||]", "New Foo(service, otherService" }; + yield return new object[] { "New Foo([||],", "New Foo(service, otherService" }; + } + } + + public static IEnumerable SomeArgumentMissingTestCases + { + get + { + yield return new object[] + { + "New Foo([||],someOtherService, someAnotherService, )", + "New Foo(service, someOtherService, someAnotherService, yetAnotherService)" + }; + yield return new object[] + { + "New Foo([||],someOtherService, someAnotherService, )", + "New Foo(service, someOtherService, someAnotherService, yetAnotherService)" + }; + yield return new object[] + { + "New Foo(,someOtherService, someAnotherService, [||])", + "New Foo(service, someOtherService, someAnotherService, yetAnotherService)" + }; + yield return new object[] + { + "New Foo([||],someOtherService, someAnotherService,", + "New Foo(service, someOtherService, someAnotherService, yetAnotherService" + }; + yield return new object[] + { + "New Foo(,someOtherService, someAnotherService, [||]", + "New Foo(service, someOtherService, someAnotherService, yetAnotherService" + }; + } + } + + public static IEnumerable SpecificArgumentMissingTestCases + { + get + { + yield return new object[] + { + "New Foo(service, [||],, someYetAnotherService)", + "New Foo(service, otherService,, someYetAnotherService)" + }; + + yield return new object[] + { + "New Foo(service, [||],, someYetAnotherService)", + "New Foo(service, otherService,, someYetAnotherService)" + }; + } + } + + protected override CodeRefactoringProvider CodeRefactoringProvider { get; } = new IntroduceSubstituteCodeRefactoringProvider(); + + [Theory] + [MemberData(nameof(AllArgumentsMissingTestCases))] + public async Task GeneratesConstructorArgumentListWithLocalVariables_WhenAllArgumentsMissing(string creationExpression, string expectedCreationExpression) + { + var source = $@"Imports NSubstitute + +Namespace MyNamespace + Interface IService + End Interface + + Interface IOtherService + End Interface + + Public Class Foo + Public Sub New(ByVal service As IService, ByVal otherService As IOtherService) + End Sub + End Class + + Public Class FooTests + Public Sub Test() + Dim target = {creationExpression} + End Sub + End Class +End Namespace +"; + var newSource = $@"Imports NSubstitute + +Namespace MyNamespace + Interface IService + End Interface + + Interface IOtherService + End Interface + + Public Class Foo + Public Sub New(ByVal service As IService, ByVal otherService As IOtherService) + End Sub + End Class + + Public Class FooTests + Public Sub Test() + Dim service = Substitute.For(Of IService)() + Dim otherService = Substitute.For(Of IOtherService)() + Dim target = {expectedCreationExpression} + End Sub + End Class +End Namespace +"; + await VerifyRefactoring(source, newSource, refactoringIndex: 2); + } + + [Fact] + public async Task GeneratesConstructorArgumentListForAllArguments_WithFullyQualifiedName_WhenNamespaceNotImported() + { + var source = @" +Namespace MyNamespace + Interface IService + End Interface + + Interface IOtherService + End Interface + + Public Class Foo + Public Sub New(ByVal service As IService, ByVal otherService As IOtherService) + End Sub + End Class + + Public Class FooTests + Public Sub Test() + Dim target = New Foo([||]) + End Sub + End Class +End Namespace"; + + var newSource = @" +Namespace MyNamespace + Interface IService + End Interface + + Interface IOtherService + End Interface + + Public Class Foo + Public Sub New(ByVal service As IService, ByVal otherService As IOtherService) + End Sub + End Class + + Public Class FooTests + Public Sub Test() + Dim service = NSubstitute.Substitute.For(Of IService)() + Dim otherService = NSubstitute.Substitute.For(Of IOtherService)() + Dim target = New Foo(service, otherService) + End Sub + End Class +End Namespace"; + + await VerifyRefactoring(source, newSource, refactoringIndex: 2); + } + + [Fact] + public async Task GeneratesConstructorArgumentListForSpecificSubstitute_WithFullyQualifiedName_WhenNamespaceNotImported() + { + var source = @"Namespace MyNamespace + Interface IService + End Interface + + Interface IOtherService + End Interface + + Public Class Foo + Public Sub New(ByVal service As IService, ByVal otherService As IOtherService) + End Sub + End Class + + Public Class FooTests + Public Sub Test() + Dim target = New Foo([||],) + End Sub + End Class +End Namespace +"; + var newSource = @"Namespace MyNamespace + Interface IService + End Interface + + Interface IOtherService + End Interface + + Public Class Foo + Public Sub New(ByVal service As IService, ByVal otherService As IOtherService) + End Sub + End Class + + Public Class FooTests + Public Sub Test() + Dim service = NSubstitute.Substitute.For(Of IService)() + Dim target = New Foo(service,) + End Sub + End Class +End Namespace +"; + await VerifyRefactoring(source, newSource, refactoringIndex: 0); + } + + [Theory] + [MemberData(nameof(SomeArgumentMissingTestCases))] + public async Task GeneratesConstructorArgumentListWithLocalVariables_WhenSomeArgumentsMissing(string creationExpression, string expectedCreationExpression) + { + var source = $@"Imports NSubstitute + +Namespace MyNamespace + Interface IService + End Interface + + Interface IOtherService + End Interface + + Interface IAnotherService + End Interface + + Interface IYetAnotherService + End Interface + + Public Class Foo + Public Sub New(ByVal service As IService, ByVal otherService As IOtherService, ByVal anotherService As IAnotherService, ByVal yetAnotherService As IYetAnotherService) + End Sub + End Class + + Public Class FooTests + Public Sub Test() + Dim someOtherService = Substitute.For(Of IOtherService)() + Dim someAnotherService = Substitute.For(Of IAnotherService)() + Dim target = {creationExpression} + End Sub + End Class +End Namespace"; + + var newSource = $@"Imports NSubstitute + +Namespace MyNamespace + Interface IService + End Interface + + Interface IOtherService + End Interface + + Interface IAnotherService + End Interface + + Interface IYetAnotherService + End Interface + + Public Class Foo + Public Sub New(ByVal service As IService, ByVal otherService As IOtherService, ByVal anotherService As IAnotherService, ByVal yetAnotherService As IYetAnotherService) + End Sub + End Class + + Public Class FooTests + Public Sub Test() + Dim service = Substitute.For(Of IService)() + Dim yetAnotherService = Substitute.For(Of IYetAnotherService)() + Dim someOtherService = Substitute.For(Of IOtherService)() + Dim someAnotherService = Substitute.For(Of IAnotherService)() + Dim target = {expectedCreationExpression} + End Sub + End Class +End Namespace"; + + await VerifyRefactoring(source, newSource, refactoringIndex: 2); + } + + [Theory] + [MemberData(nameof(AllArgumentsMissingTestCases))] + public async Task GeneratesConstructorArgumentListWithReadonlyFields_WhenAllArgumentsMissing(string creationExpression, string expectedCreationExpression) + { + var source = $@"Imports NSubstitute + +Namespace MyNamespace + Interface IService + End Interface + + Interface IOtherService + End Interface + + Public Class Foo + Public Sub New(ByVal service As IService, ByVal otherService As IOtherService) + End Sub + End Class + + Public Class FooTests + Public Sub Test() + Dim target = {creationExpression} + End Sub + End Class +End Namespace"; + + var newSource = $@"Imports NSubstitute + +Namespace MyNamespace + Interface IService + End Interface + + Interface IOtherService + End Interface + + Public Class Foo + Public Sub New(ByVal service As IService, ByVal otherService As IOtherService) + End Sub + End Class + + Public Class FooTests + Private ReadOnly service As IService = Substitute.For(Of IService)() + Private ReadOnly otherService As IOtherService = Substitute.For(Of IOtherService)() + + Public Sub Test() + Dim target = {expectedCreationExpression} + End Sub + End Class +End Namespace"; + + await VerifyRefactoring(source, newSource, refactoringIndex: 3); + } + + [Theory] + [MemberData(nameof(SomeArgumentMissingTestCases))] + public async Task GeneratesConstructorArgumentListWithReadonlyFields_WhenSomeArgumentsMissing(string creationExpression, string expectedCreationExpression) + { + var source = $@"Imports NSubstitute + +Namespace MyNamespace + Interface IService + End Interface + + Interface IOtherService + End Interface + + Interface IAnotherService + End Interface + + Interface IYetAnotherService + End Interface + + Public Class Foo + Public Sub New(ByVal service As IService, ByVal otherService As IOtherService, ByVal anotherService As IAnotherService, ByVal yetAnotherService As IYetAnotherService) + End Sub + End Class + + Public Class FooTests + Public Sub Test() + Dim someOtherService = Substitute.For(Of IOtherService)() + Dim someAnotherService = Substitute.For(Of IAnotherService)() + Dim target = {creationExpression} + End Sub + End Class +End Namespace"; + + var newSource = $@"Imports NSubstitute + +Namespace MyNamespace + Interface IService + End Interface + + Interface IOtherService + End Interface + + Interface IAnotherService + End Interface + + Interface IYetAnotherService + End Interface + + Public Class Foo + Public Sub New(ByVal service As IService, ByVal otherService As IOtherService, ByVal anotherService As IAnotherService, ByVal yetAnotherService As IYetAnotherService) + End Sub + End Class + + Public Class FooTests + Private ReadOnly service As IService = Substitute.For(Of IService)() + Private ReadOnly yetAnotherService As IYetAnotherService = Substitute.For(Of IYetAnotherService)() + + Public Sub Test() + Dim someOtherService = Substitute.For(Of IOtherService)() + Dim someAnotherService = Substitute.For(Of IAnotherService)() + Dim target = {expectedCreationExpression} + End Sub + End Class +End Namespace"; + + await VerifyRefactoring(source, newSource, refactoringIndex: 3); + } + + [Theory] + [MemberData(nameof(SpecificArgumentMissingTestCases))] + public async Task GeneratesConstructorArgumentListWithLocalVariable_ForSpecificArgument_WhenArgumentMissing(string creationExpression, string expectedCreationExpression) + { + var source = $@"Imports NSubstitute + +Namespace MyNamespace + Interface IService + End Interface + + Interface IOtherService + End Interface + + Interface IAnotherService + End Interface + + Interface IYetAnotherService + End Interface + + Public Class Foo + Public Sub New(ByVal service As IService, ByVal otherService As IOtherService, ByVal anotherService As IAnotherService, ByVal yetAnotherService As IYetAnotherService) + End Sub + End Class + + Public Class FooTests + Public Sub Test() + Dim service = Substitute.For(Of IService)() + Dim someYetAnotherService = Substitute.For(Of IYetAnotherService)() + Dim target = {creationExpression} + End Sub + End Class +End Namespace"; + + var newSource = $@"Imports NSubstitute + +Namespace MyNamespace + Interface IService + End Interface + + Interface IOtherService + End Interface + + Interface IAnotherService + End Interface + + Interface IYetAnotherService + End Interface + + Public Class Foo + Public Sub New(ByVal service As IService, ByVal otherService As IOtherService, ByVal anotherService As IAnotherService, ByVal yetAnotherService As IYetAnotherService) + End Sub + End Class + + Public Class FooTests + Public Sub Test() + Dim otherService = Substitute.For(Of IOtherService)() + Dim service = Substitute.For(Of IService)() + Dim someYetAnotherService = Substitute.For(Of IYetAnotherService)() + Dim target = {expectedCreationExpression} + End Sub + End Class +End Namespace"; + + await VerifyRefactoring(source, newSource, refactoringIndex: 0); + } + + [Theory] + [MemberData(nameof(SpecificArgumentMissingTestCases))] + public async Task GeneratesConstructorArgumentListWithReadonlyFields_ForSpecificArgument_WhenArgumentMissing(string creationExpression, string expectedCreationExpression) + { + var source = $@"Imports NSubstitute + +Namespace MyNamespace + Interface IService + End Interface + + Interface IOtherService + End Interface + + Interface IAnotherService + End Interface + + Interface IYetAnotherService + End Interface + + Public Class Foo + Public Sub New(ByVal service As IService, ByVal otherService As IOtherService, ByVal anotherService As IAnotherService, ByVal yetAnotherService As IYetAnotherService) + End Sub + End Class + + Public Class FooTests + Private ReadOnly service As IService = Substitute.For(Of IService)() + Private ReadOnly someYetAnotherService As IYetAnotherService = Substitute.For(Of IYetAnotherService)() + + Public Sub Test() + Dim target = {creationExpression} + End Sub + End Class +End Namespace"; + + var newSource = $@"Imports NSubstitute + +Namespace MyNamespace + Interface IService + End Interface + + Interface IOtherService + End Interface + + Interface IAnotherService + End Interface + + Interface IYetAnotherService + End Interface + + Public Class Foo + Public Sub New(ByVal service As IService, ByVal otherService As IOtherService, ByVal anotherService As IAnotherService, ByVal yetAnotherService As IYetAnotherService) + End Sub + End Class + + Public Class FooTests + Private ReadOnly otherService As IOtherService = Substitute.For(Of IOtherService)() + Private ReadOnly service As IService = Substitute.For(Of IService)() + Private ReadOnly someYetAnotherService As IYetAnotherService = Substitute.For(Of IYetAnotherService)() + + Public Sub Test() + Dim target = {expectedCreationExpression} + End Sub + End Class +End Namespace"; + + await VerifyRefactoring(source, newSource, refactoringIndex: 1); + } + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.VisualBasic/CodeRefactoringProvidersTests/VisualBasicCodeRefactoringProviderActionsVerifier.cs b/tests/NSubstitute.Analyzers.Tests.VisualBasic/CodeRefactoringProvidersTests/VisualBasicCodeRefactoringProviderActionsVerifier.cs new file mode 100644 index 00000000..b15bc3d6 --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.VisualBasic/CodeRefactoringProvidersTests/VisualBasicCodeRefactoringProviderActionsVerifier.cs @@ -0,0 +1,12 @@ +using NSubstitute.Analyzers.Tests.Shared.CodeRefactoringProviders; + +namespace NSubstitute.Analyzers.Tests.VisualBasic.CodeRefactoringProvidersTests +{ + public abstract class VisualBasicCodeRefactoringProviderActionsVerifier : CodeRefactoringProviderActionsVerifier + { + protected VisualBasicCodeRefactoringProviderActionsVerifier() + : base(VisualBasicWorkspaceFactory.Default) + { + } + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.VisualBasic/CodeRefactoringProvidersTests/VisualBasicCodeRefactoringProviderVerifier.cs b/tests/NSubstitute.Analyzers.Tests.VisualBasic/CodeRefactoringProvidersTests/VisualBasicCodeRefactoringProviderVerifier.cs new file mode 100644 index 00000000..accd6c08 --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.VisualBasic/CodeRefactoringProvidersTests/VisualBasicCodeRefactoringProviderVerifier.cs @@ -0,0 +1,12 @@ +using NSubstitute.Analyzers.Tests.Shared.CodeRefactoringProviders; + +namespace NSubstitute.Analyzers.Tests.VisualBasic.CodeRefactoringProvidersTests +{ + public abstract class VisualBasicCodeRefactoringProviderVerifier : CodeRefactoringProviderVerifier + { + protected VisualBasicCodeRefactoringProviderVerifier() + : base(VisualBasicWorkspaceFactory.Default) + { + } + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.VisualBasic/ConventionTests/AnalyzersConventionTests.cs b/tests/NSubstitute.Analyzers.Tests.VisualBasic/ConventionTests/AnalyzersConventionTests.cs index ad1f9cc1..c2bff806 100644 --- a/tests/NSubstitute.Analyzers.Tests.VisualBasic/ConventionTests/AnalyzersConventionTests.cs +++ b/tests/NSubstitute.Analyzers.Tests.VisualBasic/ConventionTests/AnalyzersConventionTests.cs @@ -37,5 +37,17 @@ public void CodeFixProvidersInheritanceHierarchyShouldBeSatisfied() { _fixture.AssertCodeFixProviderInheritanceFromAssemblyContaining(); } + + [Fact] + public void CodeRefactoringProvidersAttributeConventionsShouldBeSatisfied() + { + _fixture.AssertExportCodeRefactoringProviderAttributeUsageFromAssemblyContaining(LanguageNames.VisualBasic); + } + + [Fact] + public void CodeRefactoringProvidersInheritanceHierarchyShouldBeSatisfied() + { + _fixture.AssertCodeRefactoringProviderInheritanceFromAssemblyContaining(); + } } } \ No newline at end of file