diff --git a/tests/Moq.Analyzers.Test/Common/DiagnosticExtensionsTests.cs b/tests/Moq.Analyzers.Test/Common/DiagnosticExtensionsTests.cs index 283a236be..fea0b627c 100644 --- a/tests/Moq.Analyzers.Test/Common/DiagnosticExtensionsTests.cs +++ b/tests/Moq.Analyzers.Test/Common/DiagnosticExtensionsTests.cs @@ -4,9 +4,6 @@ namespace Moq.Analyzers.Test.Common; public class DiagnosticExtensionsTests { - private static readonly MetadataReference CorlibReference; - private static readonly MetadataReference SystemRuntimeReference; - #pragma warning disable RS2008 // Enable analyzer release tracking (test-only descriptor) #pragma warning disable ECS1300 // Test-only descriptor; inline init is simpler than static constructor private static readonly DiagnosticDescriptor TestRule = new( @@ -19,17 +16,6 @@ public class DiagnosticExtensionsTests #pragma warning restore ECS1300 #pragma warning restore RS2008 -#pragma warning disable S3963 // Static fields should be initialized inline - conflicts with ECS1300 - static DiagnosticExtensionsTests() - { - CorlibReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); - string runtimeDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!; - SystemRuntimeReference = MetadataReference.CreateFromFile(Path.Combine(runtimeDir, "System.Runtime.dll")); - } -#pragma warning restore S3963 - - private static MetadataReference[] CoreReferences => [CorlibReference, SystemRuntimeReference]; - // Overload #1: SyntaxNode + rule + messageArgs [Fact] public void CreateDiagnostic_FromSyntaxNode_Basic() @@ -169,7 +155,7 @@ public void CreateDiagnostic_FromOperation_DelegatesToSyntax() CSharpCompilation compilation = CSharpCompilation.Create( "Test", new[] { tree }, - CoreReferences, + CompilationHelper.CoreReferences, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); SemanticModel model = compilation.GetSemanticModel(tree); MethodDeclarationSyntax methodDecl = root.DescendantNodes().OfType().First(); @@ -189,7 +175,7 @@ public void CreateDiagnostic_FromOperation_WithProperties() CSharpCompilation compilation = CSharpCompilation.Create( "Test", new[] { tree }, - CoreReferences, + CompilationHelper.CoreReferences, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); SemanticModel model = compilation.GetSemanticModel(tree); MethodDeclarationSyntax methodDecl = root.DescendantNodes().OfType().First(); @@ -210,7 +196,7 @@ public void CreateDiagnostic_FromOperation_WithAdditionalLocationsAndProperties_ CSharpCompilation compilation = CSharpCompilation.Create( "Test", new[] { tree }, - CoreReferences, + CompilationHelper.CoreReferences, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); SemanticModel model = compilation.GetSemanticModel(tree); MethodDeclarationSyntax methodDecl = root.DescendantNodes().OfType().First(); diff --git a/tests/Moq.Analyzers.Test/Common/EventSyntaxExtensionsTests.cs b/tests/Moq.Analyzers.Test/Common/EventSyntaxExtensionsTests.cs index 6017ae068..4032a17b5 100644 --- a/tests/Moq.Analyzers.Test/Common/EventSyntaxExtensionsTests.cs +++ b/tests/Moq.Analyzers.Test/Common/EventSyntaxExtensionsTests.cs @@ -4,20 +4,6 @@ namespace Moq.Analyzers.Test.Common; public class EventSyntaxExtensionsTests { - private static readonly MetadataReference CorlibReference; - private static readonly MetadataReference SystemRuntimeReference; - -#pragma warning disable S3963 // "static fields" should be initialized inline - conflicts with ECS1300 - static EventSyntaxExtensionsTests() - { - CorlibReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); - string runtimeDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!; - SystemRuntimeReference = MetadataReference.CreateFromFile(Path.Combine(runtimeDir, "System.Runtime.dll")); - } -#pragma warning restore S3963 - - private static MetadataReference[] CoreReferences => [CorlibReference, SystemRuntimeReference]; - [Fact] public void GetEventParameterTypes_ActionDelegate_ReturnsTypeArguments() { @@ -112,7 +98,7 @@ class C { int[] Field; }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); VariableDeclaratorSyntax fieldSyntax = tree.GetRoot() .DescendantNodes().OfType().First(); IFieldSymbol field = (IFieldSymbol)model.GetDeclaredSymbol(fieldSyntax)!; @@ -204,7 +190,7 @@ class C { int[] Field; }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); KnownSymbols knownSymbols = new KnownSymbols(model.Compilation); VariableDeclaratorSyntax fieldSyntax = tree.GetRoot() .DescendantNodes().OfType().First(); @@ -227,7 +213,7 @@ void M() } void SomeMethod() {} }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); InvocationExpressionSyntax invocation = tree.GetRoot() .DescendantNodes().OfType().First(); @@ -255,7 +241,7 @@ void M() } void SomeMethod(int x) {} }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); InvocationExpressionSyntax invocation = tree.GetRoot() .DescendantNodes().OfType().First(); @@ -283,7 +269,7 @@ void M() } void SomeMethod(int x) {} }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); InvocationExpressionSyntax invocation = tree.GetRoot() .DescendantNodes().OfType().First(); @@ -319,7 +305,7 @@ void SomeMethod(int selector) {} class C { event MyDelegate MyEvent; }", "MyEvent"); - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); InvocationExpressionSyntax invocation = tree.GetRoot() .DescendantNodes().OfType().First(); @@ -354,7 +340,7 @@ void SomeMethod(int selector, int a, string b) {} class C { event MyDelegate MyEvent; }", "MyEvent"); - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); InvocationExpressionSyntax invocation = tree.GetRoot() .DescendantNodes().OfType().First(); @@ -382,7 +368,7 @@ void M() } void SomeMethod() {} }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); KnownSymbols knownSymbols = new KnownSymbols(model.Compilation); InvocationExpressionSyntax invocation = tree.GetRoot() .DescendantNodes().OfType().First(); @@ -412,7 +398,7 @@ void M() } void SomeMethod(int x) {} }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); KnownSymbols knownSymbols = new KnownSymbols(model.Compilation); InvocationExpressionSyntax invocation = tree.GetRoot() .DescendantNodes().OfType().First(); @@ -450,7 +436,7 @@ void SomeMethod(int selector, int a) {} class C { event MyDelegate MyEvent; }", "MyEvent"); - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); KnownSymbols knownSymbols = new KnownSymbols(model.Compilation); InvocationExpressionSyntax invocation = tree.GetRoot() .DescendantNodes().OfType().First(); @@ -474,22 +460,10 @@ class C { event MyDelegate MyEvent; }", // and RaisesEventArgumentsShouldMatchEventSignatureAnalyzerTests, which exercise all // branching logic (too few args, too many args, wrong type, matching types, with/without // eventName). - private static (SemanticModel Model, SyntaxTree Tree) CreateCompilation(string code) - { - SyntaxTree tree = CSharpSyntaxTree.ParseText(code); - CSharpCompilation compilation = CSharpCompilation.Create( - "TestAssembly", - new[] { tree }, - CoreReferences, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - SemanticModel model = compilation.GetSemanticModel(tree); - return (model, tree); - } - #pragma warning disable ECS0900 // Boxing needed to cast to IEventSymbol from GetDeclaredSymbol private static ITypeSymbol GetEventFieldType(string code, string eventName) { - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); VariableDeclaratorSyntax variable = tree.GetRoot() .DescendantNodes() .OfType() @@ -503,7 +477,7 @@ private static (ITypeSymbol EventType, KnownSymbols KnownSymbols) GetEventFieldT string code, string eventName) { - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); KnownSymbols knownSymbols = new KnownSymbols(model.Compilation); VariableDeclaratorSyntax variable = tree.GetRoot() .DescendantNodes() diff --git a/tests/Moq.Analyzers.Test/Common/IMethodSymbolExtensionsTests.cs b/tests/Moq.Analyzers.Test/Common/IMethodSymbolExtensionsTests.cs new file mode 100644 index 000000000..652b955e1 --- /dev/null +++ b/tests/Moq.Analyzers.Test/Common/IMethodSymbolExtensionsTests.cs @@ -0,0 +1,294 @@ +namespace Moq.Analyzers.Test.Common; + +public class IMethodSymbolExtensionsTests +{ + [Fact] + public void Overloads_MethodWithOverloads_ReturnsOnlyOtherOverloads() + { + const string code = @" +class C +{ + void M(int x) { } + void M(string x) { } + void M(int x, string y) { } +}"; + (IMethodSymbol targetMethod, _) = GetMethodAndOverloads(code, "M", 0); + + IMethodSymbol[] result = targetMethod.Overloads().ToArray(); + + Assert.Equal(2, result.Length); + Assert.DoesNotContain(targetMethod, result, SymbolEqualityComparer.Default); + Assert.All(result, m => Assert.Equal("M", m.Name)); + } + + [Fact] + public void Overloads_MethodWithNoOverloads_ReturnsEmpty() + { + const string code = @" +class C +{ + void M(int x) { } + void N(string x) { } +}"; + (IMethodSymbol targetMethod, _) = GetMethodAndOverloads(code, "M", 0); + + IMethodSymbol[] result = targetMethod.Overloads().ToArray(); + + Assert.Empty(result); + } + + [Fact] + public void Overloads_NullMethod_ReturnsEmpty() + { + IMethodSymbol? nullMethod = null; + + IMethodSymbol[] result = nullMethod.Overloads().ToArray(); + + Assert.Empty(result); + } + + [Fact] + public void Overloads_CustomComparer_UsesProvidedComparer() + { + const string code = @" +class C +{ + void M(int x) { } + void M(string x) { } + void M(int x, string y) { } +}"; + (IMethodSymbol targetMethod, _) = GetMethodAndOverloads(code, "M", 0); + + // SymbolEqualityComparer.IncludeNullability behaves the same as Default for non-nullable types, + // but this verifies the comparer parameter is threaded through. + IMethodSymbol[] result = targetMethod.Overloads(SymbolEqualityComparer.IncludeNullability).ToArray(); + + Assert.Equal(2, result.Length); + Assert.DoesNotContain(targetMethod, result, SymbolEqualityComparer.Default); + } + + [Fact] + public void TryGetOverloadWithParameterOfType_OverloadHasMatchingParameter_ReturnsTrueWithMatches() + { + const string code = @" +class C +{ + void M(int x) { } + void M(string x) { } + void M(int x, string y) { } +}"; + (SemanticModel model, IMethodSymbol intMethod, IReadOnlyList allMethods) = + GetMethodContextWithAllOverloads(code, "M", 0); + INamedTypeSymbol stringType = model.Compilation.GetSpecialType(SpecialType.System_String); + + bool found = intMethod.TryGetOverloadWithParameterOfType( + allMethods, + stringType, + out IMethodSymbol? methodMatch, + out IParameterSymbol? parameterMatch); + + Assert.True(found); + Assert.NotNull(methodMatch); + Assert.NotNull(parameterMatch); + Assert.True(SymbolEqualityComparer.Default.Equals(parameterMatch!.Type, stringType)); + } + + [Fact] + public void TryGetOverloadWithParameterOfType_NoOverloadHasMatchingParameter_ReturnsFalse() + { + const string code = @" +class C +{ + void M(int x) { } + void M(string x) { } +}"; + (SemanticModel model, IMethodSymbol intMethod, IReadOnlyList allMethods) = + GetMethodContextWithAllOverloads(code, "M", 0); + INamedTypeSymbol doubleType = model.Compilation.GetSpecialType(SpecialType.System_Double); + + bool found = intMethod.TryGetOverloadWithParameterOfType( + allMethods, + doubleType, + out IMethodSymbol? methodMatch, + out IParameterSymbol? parameterMatch); + + Assert.False(found); + Assert.Null(methodMatch); + Assert.Null(parameterMatch); + } + + [Fact] + public void TryGetOverloadWithParameterOfType_CurrentMethodInOverloadsList_SkipsSelf() + { + const string code = @" +class C +{ + void M(int x) { } +}"; + (SemanticModel model, IMethodSymbol intMethod, _) = + GetMethodContextWithAllOverloads(code, "M", 0); + INamedTypeSymbol intType = model.Compilation.GetSpecialType(SpecialType.System_Int32); + + // Pass the method itself in the overloads list. It should be skipped via comparer.Equals. + bool found = intMethod.TryGetOverloadWithParameterOfType( + new[] { intMethod }, + intType, + out IMethodSymbol? methodMatch, + out IParameterSymbol? parameterMatch); + + Assert.False(found); + Assert.Null(methodMatch); + Assert.Null(parameterMatch); + } + + [Fact] + public void TryGetOverloadWithParameterOfType_CancellationRequested_ThrowsOperationCanceledException() + { + const string code = @" +class C +{ + void M(int x) { } + void M(string x) { } +}"; + (SemanticModel model, IMethodSymbol intMethod, IReadOnlyList allMethods) = + GetMethodContextWithAllOverloads(code, "M", 0); + INamedTypeSymbol stringType = model.Compilation.GetSpecialType(SpecialType.System_String); + + using CancellationTokenSource cts = new(); + cts.Cancel(); + + Assert.Throws(() => + { + intMethod.TryGetOverloadWithParameterOfType( + allMethods, + stringType, + out _, + out _, + cancellationToken: cts.Token); + }); + } + + [Fact] + public void TryGetOverloadWithParameterOfType_ConvenienceOverload_DelegatesToFullVersion() + { + const string code = @" +class C +{ + void M(int x) { } + void M(string x) { } + void M(int x, string y) { } +}"; + (SemanticModel model, IMethodSymbol intMethod, _) = + GetMethodContextWithAllOverloads(code, "M", 0); + INamedTypeSymbol stringType = model.Compilation.GetSpecialType(SpecialType.System_String); + + bool found = intMethod.TryGetOverloadWithParameterOfType( + stringType, + out IMethodSymbol? methodMatch, + out IParameterSymbol? parameterMatch); + + Assert.True(found); + Assert.NotNull(methodMatch); + Assert.NotNull(parameterMatch); + Assert.True(SymbolEqualityComparer.Default.Equals(parameterMatch!.Type, stringType)); + } + + [Fact] + public void TryGetParameterOfType_MethodHasParameterOfType_ReturnsTrueWithMatch() + { + const string code = @" +class C +{ + void M(int x, string y) { } +}"; + (SemanticModel model, IMethodSymbol method, _) = + GetMethodContextWithAllOverloads(code, "M", 0); + INamedTypeSymbol stringType = model.Compilation.GetSpecialType(SpecialType.System_String); + + bool found = method.TryGetParameterOfType(stringType, out IParameterSymbol? match); + + Assert.True(found); + Assert.NotNull(match); + Assert.Equal("y", match!.Name); + Assert.True(SymbolEqualityComparer.Default.Equals(match.Type, stringType)); + } + + [Fact] + public void TryGetParameterOfType_MethodHasNoParameterOfType_ReturnsFalse() + { + const string code = @" +class C +{ + void M(int x) { } +}"; + (SemanticModel model, IMethodSymbol method, _) = + GetMethodContextWithAllOverloads(code, "M", 0); + INamedTypeSymbol doubleType = model.Compilation.GetSpecialType(SpecialType.System_Double); + + bool found = method.TryGetParameterOfType(doubleType, out IParameterSymbol? match); + + Assert.False(found); + Assert.Null(match); + } + + [Fact] + public void TryGetParameterOfType_CancellationRequested_ThrowsOperationCanceledException() + { + const string code = @" +class C +{ + void M(int x, string y) { } +}"; + (SemanticModel model, IMethodSymbol method, _) = + GetMethodContextWithAllOverloads(code, "M", 0); + INamedTypeSymbol stringType = model.Compilation.GetSpecialType(SpecialType.System_String); + + using CancellationTokenSource cts = new(); + cts.Cancel(); + + Assert.Throws(() => + { + method.TryGetParameterOfType(stringType, out _, cancellationToken: cts.Token); + }); + } + + private static (IMethodSymbol TargetMethod, IReadOnlyList AllOverloads) GetMethodAndOverloads( + string code, + string methodName, + int index) + { + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); + MethodDeclarationSyntax[] methods = tree.GetRoot() + .DescendantNodes() + .OfType() + .Where(m => string.Equals(m.Identifier.Text, methodName, StringComparison.Ordinal)) + .ToArray(); + + IMethodSymbol target = model.GetDeclaredSymbol(methods[index])!; + IMethodSymbol[] allOverloads = methods + .Select(m => model.GetDeclaredSymbol(m)!) + .ToArray(); + + return (target, allOverloads); + } + + private static (SemanticModel Model, IMethodSymbol TargetMethod, IReadOnlyList AllMethods) GetMethodContextWithAllOverloads( + string code, + string methodName, + int index) + { + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); + MethodDeclarationSyntax[] methods = tree.GetRoot() + .DescendantNodes() + .OfType() + .Where(m => string.Equals(m.Identifier.Text, methodName, StringComparison.Ordinal)) + .ToArray(); + + IMethodSymbol target = model.GetDeclaredSymbol(methods[index])!; + IMethodSymbol[] allMethods = methods + .Select(m => model.GetDeclaredSymbol(m)!) + .ToArray(); + + return (model, target, allMethods); + } +} diff --git a/tests/Moq.Analyzers.Test/Common/IOperationExtensionsTests.cs b/tests/Moq.Analyzers.Test/Common/IOperationExtensionsTests.cs index dfc152ac3..48acdfac9 100644 --- a/tests/Moq.Analyzers.Test/Common/IOperationExtensionsTests.cs +++ b/tests/Moq.Analyzers.Test/Common/IOperationExtensionsTests.cs @@ -4,20 +4,6 @@ namespace Moq.Analyzers.Test.Common; public class IOperationExtensionsTests { - private static readonly MetadataReference CorlibReference; - private static readonly MetadataReference SystemRuntimeReference; - -#pragma warning disable S3963 // "static fields" should be initialized inline - conflicts with ECS1300 - static IOperationExtensionsTests() - { - CorlibReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); - string runtimeDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!; - SystemRuntimeReference = MetadataReference.CreateFromFile(Path.Combine(runtimeDir, "System.Runtime.dll")); - } -#pragma warning restore S3963 - - private static MetadataReference[] CoreReferences => [CorlibReference, SystemRuntimeReference]; - [Fact] public void WalkDownConversion_NonConversionOperation_ReturnsSelf() { @@ -48,7 +34,7 @@ void M() long y = x; } }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); VariableDeclaratorSyntax declarator = tree.GetRoot() .DescendantNodes().OfType() .First(v => string.Equals(v.Identifier.Text, "y", StringComparison.Ordinal)); @@ -78,7 +64,7 @@ void M() long y = b; } }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); VariableDeclaratorSyntax declarator = tree.GetRoot() .DescendantNodes().OfType() .First(v => string.Equals(v.Identifier.Text, "y", StringComparison.Ordinal)); @@ -120,7 +106,7 @@ void M() long y = x; } }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); VariableDeclaratorSyntax declarator = tree.GetRoot() .DescendantNodes().OfType() .First(v => string.Equals(v.Identifier.Text, "y", StringComparison.Ordinal)); @@ -150,7 +136,7 @@ void M() short y = (short)x; } }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); CastExpressionSyntax castExpr = tree.GetRoot() .DescendantNodes().OfType().First(); IOperation? castOp = model.GetOperation(castExpr); @@ -174,7 +160,7 @@ void M() long y = b; } }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); VariableDeclaratorSyntax declarator = tree.GetRoot() .DescendantNodes().OfType() .First(v => string.Equals(v.Identifier.Text, "y", StringComparison.Ordinal)); @@ -610,21 +596,9 @@ void M() Assert.Contains("_field", result!.ToString(), StringComparison.Ordinal); } - private static (SemanticModel Model, SyntaxTree Tree) CreateCompilation(string code) - { - SyntaxTree tree = CSharpSyntaxTree.ParseText(code); - CSharpCompilation compilation = CSharpCompilation.Create( - "TestAssembly", - new[] { tree }, - CoreReferences, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - SemanticModel model = compilation.GetSemanticModel(tree); - return (model, tree); - } - private static IAnonymousFunctionOperation GetLambdaOperation(string code) { - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); LambdaExpressionSyntax lambda = GetFirstLambda(tree); IOperation? lambdaOp = model.GetOperation(lambda); Assert.NotNull(lambdaOp); @@ -635,7 +609,7 @@ private static IAnonymousFunctionOperation GetLambdaOperation(string code) private static T GetFirstOperationOfType(string code) where T : IOperation { - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); IEnumerable allOperations = tree.GetRoot() .DescendantNodes() .Select(node => model.GetOperation(node)) diff --git a/tests/Moq.Analyzers.Test/Common/ISymbolExtensionsTests.cs b/tests/Moq.Analyzers.Test/Common/ISymbolExtensionsTests.cs index 719fbdfdf..a9fcfafa7 100644 --- a/tests/Moq.Analyzers.Test/Common/ISymbolExtensionsTests.cs +++ b/tests/Moq.Analyzers.Test/Common/ISymbolExtensionsTests.cs @@ -1,5 +1,4 @@ using System.Collections.Immutable; -using Microsoft.CodeAnalysis.Testing; using Moq.Analyzers.Common.WellKnown; using Moq.Analyzers.Test.Helpers; @@ -7,24 +6,13 @@ namespace Moq.Analyzers.Test.Common; public class ISymbolExtensionsTests { - private static readonly MetadataReference CorlibReference; - private static readonly MetadataReference SystemRuntimeReference; - private static readonly MetadataReference SystemThreadingTasksReference; - private static readonly MetadataReference SystemLinqReference; +#pragma warning disable ECS1300 // Static field init is simpler than static constructor for single field + private static readonly MetadataReference SystemThreadingTasksReference = + MetadataReference.CreateFromFile(typeof(System.Threading.Tasks.Task).Assembly.Location); +#pragma warning restore ECS1300 -#pragma warning disable S3963 // "static fields" should be initialized inline - conflicts with ECS1300 - static ISymbolExtensionsTests() - { - CorlibReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); - string runtimeDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!; - SystemRuntimeReference = MetadataReference.CreateFromFile(Path.Combine(runtimeDir, "System.Runtime.dll")); - SystemThreadingTasksReference = MetadataReference.CreateFromFile(typeof(System.Threading.Tasks.Task).Assembly.Location); - SystemLinqReference = MetadataReference.CreateFromFile(typeof(System.Linq.Enumerable).Assembly.Location); - } -#pragma warning restore S3963 - - private static MetadataReference[] CoreReferences => - [CorlibReference, SystemRuntimeReference, SystemThreadingTasksReference, SystemLinqReference]; + private static MetadataReference[] CoreReferencesWithTasks => + [CompilationHelper.CorlibReference, CompilationHelper.SystemRuntimeReference, SystemThreadingTasksReference, CompilationHelper.SystemLinqReference]; [Fact] public void IsConstructor_PublicConstructor_ReturnsTrue() @@ -57,7 +45,7 @@ public void IsConstructor_PrivateConstructor_ReturnsFalse() [Fact] public void IsConstructor_StaticConstructor_ReturnsFalse() { - (SemanticModel model, SyntaxTree tree) = CreateCompilation("public class C { static C() {} }"); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation("public class C { static C() {} }"); IMethodSymbol staticCtor = GetMethodSymbols(model, tree) .First(m => m.MethodKind == MethodKind.StaticConstructor); Assert.False(((ISymbol)staticCtor).IsConstructor()); @@ -66,7 +54,7 @@ public void IsConstructor_StaticConstructor_ReturnsFalse() [Fact] public void IsConstructor_RegularMethod_ReturnsFalse() { - (SemanticModel model, SyntaxTree tree) = CreateCompilation("public class C { public void M() {} }"); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation("public class C { public void M() {} }"); IMethodSymbol method = GetMethodSymbols(model, tree) .First(m => string.Equals(m.Name, "M", StringComparison.Ordinal)); Assert.False(((ISymbol)method).IsConstructor()); @@ -75,7 +63,7 @@ public void IsConstructor_RegularMethod_ReturnsFalse() [Fact] public void IsConstructor_Property_ReturnsFalse() { - (SemanticModel model, SyntaxTree tree) = CreateCompilation("public class C { public int P { get; set; } }"); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation("public class C { public int P { get; set; } }"); IPropertySymbol prop = tree.GetRoot() .DescendantNodes() .OfType() @@ -88,7 +76,7 @@ public void IsConstructor_Property_ReturnsFalse() [Fact] public void IsInstanceOf_NullSymbol_ReturnsFalse() { - (SemanticModel model, SyntaxTree tree) = CreateCompilation("public class C { public void M() {} }"); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation("public class C { public void M() {} }"); IMethodSymbol method = GetMethodSymbols(model, tree) .First(m => string.Equals(m.Name, "M", StringComparison.Ordinal)); ISymbol? nullSymbol = null; @@ -98,7 +86,7 @@ public void IsInstanceOf_NullSymbol_ReturnsFalse() [Fact] public void IsInstanceOf_NullOther_ReturnsFalse() { - (SemanticModel model, SyntaxTree tree) = CreateCompilation("public class C { public void M() {} }"); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation("public class C { public void M() {} }"); IMethodSymbol method = GetMethodSymbols(model, tree) .First(m => string.Equals(m.Name, "M", StringComparison.Ordinal)); Assert.False(method.IsInstanceOf(null)); @@ -113,7 +101,7 @@ public class C public void M(T value) {} public void Caller() { M(42); } }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); InvocationExpressionSyntax invocation = tree.GetRoot() .DescendantNodes().OfType().First(); IMethodSymbol? calledMethod = model.GetSymbolInfo(invocation).Symbol as IMethodSymbol; @@ -131,7 +119,7 @@ public class C public void M1() {} public void M2() {} }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); IMethodSymbol[] methods = GetMethodSymbols(model, tree) .Where(m => m.Name.StartsWith("M", StringComparison.Ordinal)) .ToArray(); @@ -150,7 +138,7 @@ public class C { public void Caller() { ""hello"".DoSomething(); } }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); InvocationExpressionSyntax invocation = tree.GetRoot() .DescendantNodes().OfType().First(); IMethodSymbol? reducedMethod = model.GetSymbolInfo(invocation).Symbol as IMethodSymbol; @@ -171,7 +159,7 @@ public class C public void M(T value) {} public void Caller() { M(42); } }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); InvocationExpressionSyntax invocation = tree.GetRoot() .DescendantNodes().OfType().First(); IMethodSymbol? calledMethod = model.GetSymbolInfo(invocation).Symbol as IMethodSymbol; @@ -189,7 +177,7 @@ public class C { public void M(int a, string b) {} }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); IMethodSymbol method = GetMethodSymbols(model, tree) .First(m => string.Equals(m.Name, "M", StringComparison.Ordinal)); IParameterSymbol paramA = method.Parameters[0]; @@ -207,7 +195,7 @@ public class C { public List Prop { get; set; } }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); PropertyDeclarationSyntax propSyntax = tree.GetRoot() .DescendantNodes().OfType().First(); IPropertySymbol propSymbol = model.GetDeclaredSymbol(propSyntax)!; @@ -221,7 +209,7 @@ public class C public void IsInstanceOf_NonGenericNamedType_ReturnsTrue() { string code = "public class MyClass {}"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); ClassDeclarationSyntax classSyntax = tree.GetRoot() .DescendantNodes().OfType().First(); INamedTypeSymbol type = model.GetDeclaredSymbol(classSyntax)!; @@ -235,7 +223,7 @@ public void IsInstanceOf_NamedTypeNotMatching_ReturnsFalse() string code = @" public class A {} public class B {}"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); INamedTypeSymbol[] types = tree.GetRoot() .DescendantNodes().OfType() .Select(c => model.GetDeclaredSymbol(c)!) @@ -248,7 +236,7 @@ public class B {}"; public void IsInstanceOf_DefaultFallback_FieldSymbolMatching_ReturnsTrue() { string code = "public class C { public int F; }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); VariableDeclaratorSyntax fieldSyntax = tree.GetRoot() .DescendantNodes().OfType().First(); IFieldSymbol field = (IFieldSymbol)model.GetDeclaredSymbol(fieldSyntax)!; @@ -260,7 +248,7 @@ public void IsInstanceOf_DefaultFallback_FieldSymbolMatching_ReturnsTrue() public void IsInstanceOf_DefaultFallback_FieldSymbolNotMatching_ReturnsFalse() { string code = "public class C { public int F1; public int F2; }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); IFieldSymbol[] fields = tree.GetRoot() .DescendantNodes().OfType() .Select(v => (IFieldSymbol)model.GetDeclaredSymbol(v)!) @@ -278,7 +266,7 @@ public class C public void M1() {} public void M2() {} }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); IMethodSymbol[] methods = GetMethodSymbols(model, tree) .Where(m => m.Name.StartsWith("M", StringComparison.Ordinal)) .ToArray(); @@ -303,7 +291,7 @@ public void M1() {} public void M2() {} public void M3() {} }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); IMethodSymbol[] methods = GetMethodSymbols(model, tree) .Where(m => m.Name.StartsWith("M", StringComparison.Ordinal)) .OrderBy(m => m.Name, StringComparer.Ordinal) @@ -320,7 +308,7 @@ public void M3() {} public void IsInstanceOf_ImmutableArray_EmptyArray_ReturnsFalse() { string code = "public class C { public void M() {} }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); IMethodSymbol method = GetMethodSymbols(model, tree) .First(m => string.Equals(m.Name, "M", StringComparison.Ordinal)); ImmutableArray empty = ImmutableArray.Empty; @@ -335,7 +323,7 @@ public void IsInstanceOf_ImmutableArray_EmptyArray_ReturnsFalse() public void IsInstanceOf_ImmutableArraySimple_MatchFound_ReturnsTrue() { string code = "public class C { public void M() {} }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); IMethodSymbol method = GetMethodSymbols(model, tree) .First(m => string.Equals(m.Name, "M", StringComparison.Ordinal)); ImmutableArray candidates = ImmutableArray.Create(method); @@ -352,7 +340,7 @@ public class C public void M1() {} public void M2() {} }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); IMethodSymbol[] methods = GetMethodSymbols(model, tree) .Where(m => m.Name.StartsWith("M", StringComparison.Ordinal)) .ToArray(); @@ -364,7 +352,7 @@ public void M2() {} [Fact] public void IsOverridable_StaticMethod_ReturnsFalse() { - (SemanticModel model, SyntaxTree tree) = CreateCompilation( + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation( "public class C { public static void M() {} }"); IMethodSymbol method = GetMethodSymbols(model, tree) .First(m => string.Equals(m.Name, "M", StringComparison.Ordinal)); @@ -374,7 +362,7 @@ public void IsOverridable_StaticMethod_ReturnsFalse() [Fact] public void IsOverridable_StaticProperty_ReturnsFalse() { - (SemanticModel model, SyntaxTree tree) = CreateCompilation( + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation( "public class C { public static int P { get; set; } }"); IPropertySymbol prop = tree.GetRoot() .DescendantNodes().OfType() @@ -387,7 +375,7 @@ public void IsOverridable_StaticProperty_ReturnsFalse() [Fact] public void IsOverridable_InterfaceMember_ReturnsTrue() { - (SemanticModel model, SyntaxTree tree) = CreateCompilation( + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation( "public interface I { void M(); }"); IMethodSymbol method = GetMethodSymbols(model, tree) .First(m => string.Equals(m.Name, "M", StringComparison.Ordinal)); @@ -397,7 +385,7 @@ public void IsOverridable_InterfaceMember_ReturnsTrue() [Fact] public void IsOverridable_VirtualMethod_ReturnsTrue() { - (SemanticModel model, SyntaxTree tree) = CreateCompilation( + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation( "public class C { public virtual void M() {} }"); IMethodSymbol method = GetMethodSymbols(model, tree) .First(m => string.Equals(m.Name, "M", StringComparison.Ordinal)); @@ -407,7 +395,7 @@ public void IsOverridable_VirtualMethod_ReturnsTrue() [Fact] public void IsOverridable_AbstractMethod_ReturnsTrue() { - (SemanticModel model, SyntaxTree tree) = CreateCompilation( + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation( "public abstract class C { public abstract void M(); }"); IMethodSymbol method = GetMethodSymbols(model, tree) .First(m => string.Equals(m.Name, "M", StringComparison.Ordinal)); @@ -420,7 +408,7 @@ public void IsOverridable_OverrideMethod_ReturnsTrue() string code = @" public class Base { public virtual void M() {} } public class Derived : Base { public override void M() {} }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); IMethodSymbol method = GetMethodSymbols(model, tree) .First(m => string.Equals(m.Name, "M", StringComparison.Ordinal) && m.IsOverride); Assert.True(((ISymbol)method).IsOverridable()); @@ -432,7 +420,7 @@ public void IsOverridable_SealedOverrideMethod_ReturnsFalse() string code = @" public class Base { public virtual void M() {} } public class Derived : Base { public sealed override void M() {} }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); IMethodSymbol method = GetMethodSymbols(model, tree) .First(m => string.Equals(m.Name, "M", StringComparison.Ordinal) && m.IsSealed); Assert.False(((ISymbol)method).IsOverridable()); @@ -441,7 +429,7 @@ public class Derived : Base { public sealed override void M() {} }"; [Fact] public void IsOverridable_RegularNonVirtualMethod_ReturnsFalse() { - (SemanticModel model, SyntaxTree tree) = CreateCompilation( + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation( "public class C { public void M() {} }"); IMethodSymbol method = GetMethodSymbols(model, tree) .First(m => string.Equals(m.Name, "M", StringComparison.Ordinal)); @@ -451,7 +439,7 @@ public void IsOverridable_RegularNonVirtualMethod_ReturnsFalse() [Fact] public void IsOverridable_InterfaceProperty_ReturnsTrue() { - (SemanticModel model, SyntaxTree tree) = CreateCompilation( + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation( "public interface I { int P { get; } }"); IPropertySymbol prop = tree.GetRoot() .DescendantNodes().OfType() @@ -525,7 +513,7 @@ public void M() Task t = Task.FromResult(42); } }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); InvocationExpressionSyntax invocation = tree.GetRoot() .DescendantNodes().OfType().First(); @@ -549,7 +537,7 @@ public class C {{ public {typeName} GetValue() => default!; }}"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); MethodDeclarationSyntax methodSyntax = tree.GetRoot() .DescendantNodes().OfType().First(); @@ -568,7 +556,7 @@ public class C { public int[] GetValues() => new[] { 1 }; }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); MethodDeclarationSyntax methodSyntax = tree.GetRoot() .DescendantNodes().OfType().First(); @@ -806,7 +794,7 @@ public void M() [Fact] public async Task IsMoqRaisesMethod_NonMethodSymbol_ReturnsFalse() { - (SemanticModel model, SyntaxTree tree) = await CreateMoqCompilationAsync(@" + (SemanticModel model, SyntaxTree tree) = await CompilationHelper.CreateMoqCompilationAsync(@" using Moq; public interface IService { int Value { get; } } public class C { }"); @@ -855,41 +843,85 @@ public void M() Assert.True(symbol.IsMoqVerificationMethod(knownSymbols)); } - private static (SemanticModel Model, SyntaxTree Tree) CreateCompilation(string code) + [Fact] + public async Task IsMoqReturnValueSpecificationMethod_Throws_ReturnsTrue() + { + string code = @" +using Moq; +using System; +public interface IService { int GetValue(); } +public class C +{ + public void M() { - SyntaxTree tree = CSharpSyntaxTree.ParseText(code); - CSharpCompilation compilation = CSharpCompilation.Create( - "TestAssembly", - new[] { tree }, - CoreReferences, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - SemanticModel model = compilation.GetSemanticModel(tree); - return (model, tree); + var mock = new Mock(); + mock.Setup(x => x.GetValue()).Throws(new Exception()); + } +}"; + (ISymbol symbol, MoqKnownSymbols knownSymbols) = await GetMoqInvocationSymbol(code, "Throws"); + Assert.True(symbol.IsMoqReturnValueSpecificationMethod(knownSymbols)); + } + + [Fact] + public async Task IsMoqReturnValueSpecificationMethod_ReturnsAsync_ReturnsTrue() + { + string code = @" +using Moq; +using System.Threading.Tasks; +public interface IService { Task GetValueAsync(); } +public class C +{ + public void M() + { + var mock = new Mock(); + mock.Setup(x => x.GetValueAsync()).ReturnsAsync(1); + } +}"; + (ISymbol symbol, MoqKnownSymbols knownSymbols) = await GetMoqInvocationSymbol(code, "ReturnsAsync"); + Assert.True(symbol.IsMoqReturnValueSpecificationMethod(knownSymbols)); } - private static async Task<(SemanticModel Model, SyntaxTree Tree)> CreateMoqCompilationAsync(string code) + [Fact] + public async Task IsMoqReturnValueSpecificationMethod_ThrowsAsync_ReturnsTrue() + { + string code = @" +using Moq; +using System; +using System.Threading.Tasks; +public interface IService { Task GetValueAsync(); } +public class C +{ + public void M() { - SyntaxTree tree = CSharpSyntaxTree.ParseText(code); - MetadataReference[] references = await GetMoqReferencesAsync().ConfigureAwait(false); - CSharpCompilation compilation = CSharpCompilation.Create( - "TestAssembly", - new[] { tree }, - references, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - SemanticModel model = compilation.GetSemanticModel(tree); - return (model, tree); + var mock = new Mock(); + mock.Setup(x => x.GetValueAsync()).ThrowsAsync(new Exception()); + } +}"; + (ISymbol symbol, MoqKnownSymbols knownSymbols) = await GetMoqInvocationSymbol(code, "ThrowsAsync"); + Assert.True(symbol.IsMoqReturnValueSpecificationMethod(knownSymbols)); } - private static async Task GetMoqReferencesAsync() + [Fact] + public async Task IsMoqVerificationMethod_VerifySet_ReturnsTrue() { - ReferenceAssemblies referenceAssemblies = ReferenceAssemblyCatalog.Catalog[ReferenceAssemblyCatalog.Net80WithNewMoq]; - ImmutableArray resolved = await referenceAssemblies.ResolveAsync(LanguageNames.CSharp, CancellationToken.None).ConfigureAwait(false); - return [.. resolved]; + string code = @" +using Moq; +public interface IService { string Name { get; set; } } +public class C +{ + public void M() + { + var mock = new Mock(); + mock.VerifySet(x => x.Name = It.IsAny()); + } +}"; + (ISymbol symbol, MoqKnownSymbols knownSymbols) = await GetMoqInvocationSymbol(code, "VerifySet"); + Assert.True(symbol.IsMoqVerificationMethod(knownSymbols)); } private static IMethodSymbol GetConstructor(string code, Accessibility expectedAccessibility) { - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); return GetMethodSymbols(model, tree) .First(m => m.MethodKind == MethodKind.Constructor && m.DeclaredAccessibility == expectedAccessibility); @@ -909,7 +941,7 @@ private static (IPropertySymbol Property, MoqKnownSymbols KnownSymbols) GetPrope string code, string propertyName) { - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CoreReferencesWithTasks); MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); MemberAccessExpressionSyntax memberAccess = tree.GetRoot() .DescendantNodes().OfType() @@ -922,7 +954,7 @@ private static (IPropertySymbol Property, MoqKnownSymbols KnownSymbols) GetPrope string code, string methodName) { - (SemanticModel model, SyntaxTree tree) = await CreateMoqCompilationAsync(code).ConfigureAwait(false); + (SemanticModel model, SyntaxTree tree) = await CompilationHelper.CreateMoqCompilationAsync(code).ConfigureAwait(false); MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); SyntaxNode root = await tree.GetRootAsync().ConfigureAwait(false); InvocationExpressionSyntax invocation = root diff --git a/tests/Moq.Analyzers.Test/Common/ITypeSymbolExtensionsTests.cs b/tests/Moq.Analyzers.Test/Common/ITypeSymbolExtensionsTests.cs new file mode 100644 index 000000000..605d8ab45 --- /dev/null +++ b/tests/Moq.Analyzers.Test/Common/ITypeSymbolExtensionsTests.cs @@ -0,0 +1,56 @@ +namespace Moq.Analyzers.Test.Common; + +public class ITypeSymbolExtensionsTests +{ + [Fact] + public void GetBaseTypesAndThis_ClassWithNoExplicitBase_ReturnsClassAndObject() + { + INamedTypeSymbol classSymbol = GetNamedTypeSymbol("public class MyClass { }", "MyClass"); + + List result = classSymbol.GetBaseTypesAndThis().ToList(); + + Assert.Equal(2, result.Count); + Assert.Equal("MyClass", result[0].Name); + Assert.Equal("Object", result[1].Name); + } + + [Fact] + public void GetBaseTypesAndThis_ClassWithInheritanceChain_ReturnsAllTypesInOrder() + { + string code = @" +public class GrandParent { } +public class Parent : GrandParent { } +public class Child : Parent { }"; + INamedTypeSymbol classSymbol = GetNamedTypeSymbol(code, "Child"); + + List result = classSymbol.GetBaseTypesAndThis().ToList(); + + Assert.Equal(4, result.Count); + Assert.Equal("Child", result[0].Name); + Assert.Equal("Parent", result[1].Name); + Assert.Equal("GrandParent", result[2].Name); + Assert.Equal("Object", result[3].Name); + } + + [Fact] + public void GetBaseTypesAndThis_Interface_ReturnsOnlyInterface() + { + INamedTypeSymbol interfaceSymbol = GetNamedTypeSymbol("public interface IMyInterface { }", "IMyInterface"); + + List result = interfaceSymbol.GetBaseTypesAndThis().ToList(); + + Assert.Single(result); + Assert.Equal("IMyInterface", result[0].Name); + } + + private static INamedTypeSymbol GetNamedTypeSymbol(string code, string typeName) + { + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); + return tree.GetRoot() + .DescendantNodes() + .OfType() + .Select(t => model.GetDeclaredSymbol(t)) + .OfType() + .First(t => string.Equals(t.Name, typeName, StringComparison.Ordinal)); + } +} diff --git a/tests/Moq.Analyzers.Test/Common/InvocationExpressionSyntaxExtensionsTests.cs b/tests/Moq.Analyzers.Test/Common/InvocationExpressionSyntaxExtensionsTests.cs new file mode 100644 index 000000000..8a53e4cb9 --- /dev/null +++ b/tests/Moq.Analyzers.Test/Common/InvocationExpressionSyntaxExtensionsTests.cs @@ -0,0 +1,196 @@ +using Moq.Analyzers.Common.WellKnown; + +namespace Moq.Analyzers.Test.Common; + +public class InvocationExpressionSyntaxExtensionsTests +{ + [Fact] + public void FindMockedMethodInvocationFromSetupMethod_ValidLambdaWithMethodInvocation_ReturnsInvocation() + { + const string code = @"mock.Setup(x => x.Method())"; + SyntaxTree tree = CSharpSyntaxTree.ParseText(code, new CSharpParseOptions(kind: SourceCodeKind.Script)); + InvocationExpressionSyntax setupInvocation = tree.GetRoot() + .DescendantNodes().OfType() + .First(i => i.Expression.ToString().Contains("Setup")); + + InvocationExpressionSyntax? result = setupInvocation.FindMockedMethodInvocationFromSetupMethod(); + + Assert.NotNull(result); + Assert.Equal("x.Method()", result.ToString()); + } + + [Fact] + public void FindMockedMethodInvocationFromSetupMethod_NullInput_ReturnsNull() + { + InvocationExpressionSyntax? nullInvocation = null; + + InvocationExpressionSyntax? result = nullInvocation.FindMockedMethodInvocationFromSetupMethod(); + + Assert.Null(result); + } + + [Fact] + public void FindMockedMethodInvocationFromSetupMethod_NonLambdaArgument_ReturnsNull() + { + // Setup with a non-lambda argument (string literal) + const string code = @"mock.Setup(""notALambda"")"; + SyntaxTree tree = CSharpSyntaxTree.ParseText(code, new CSharpParseOptions(kind: SourceCodeKind.Script)); + InvocationExpressionSyntax setupInvocation = tree.GetRoot() + .DescendantNodes().OfType().First(); + + InvocationExpressionSyntax? result = setupInvocation.FindMockedMethodInvocationFromSetupMethod(); + + Assert.Null(result); + } + + [Fact] + public void FindMockedMethodInvocationFromSetupMethod_LambdaBodyIsPropertyAccess_ReturnsNull() + { + // Lambda body is a MemberAccessExpressionSyntax, not an InvocationExpressionSyntax + const string code = @"mock.Setup(x => x.Property)"; + SyntaxTree tree = CSharpSyntaxTree.ParseText(code, new CSharpParseOptions(kind: SourceCodeKind.Script)); + InvocationExpressionSyntax setupInvocation = tree.GetRoot() + .DescendantNodes().OfType().First(); + + InvocationExpressionSyntax? result = setupInvocation.FindMockedMethodInvocationFromSetupMethod(); + + Assert.Null(result); + } + + [Fact] + public void FindMockedMemberExpressionFromSetupMethod_ValidLambda_ReturnsMemberExpression() + { + const string code = @"mock.Setup(x => x.Property)"; + SyntaxTree tree = CSharpSyntaxTree.ParseText(code, new CSharpParseOptions(kind: SourceCodeKind.Script)); + InvocationExpressionSyntax setupInvocation = tree.GetRoot() + .DescendantNodes().OfType().First(); + + ExpressionSyntax? result = setupInvocation.FindMockedMemberExpressionFromSetupMethod(); + + Assert.NotNull(result); + Assert.Equal("x.Property", result.ToString()); + } + + [Fact] + public void FindMockedMemberExpressionFromSetupMethod_NullInput_ReturnsNull() + { + InvocationExpressionSyntax? nullInvocation = null; + + ExpressionSyntax? result = nullInvocation.FindMockedMemberExpressionFromSetupMethod(); + + Assert.Null(result); + } + + [Fact] + public void FindMockedMemberExpressionFromSetupMethod_NonLambdaArgument_ReturnsNull() + { + const string code = @"mock.Setup(""notALambda"")"; + SyntaxTree tree = CSharpSyntaxTree.ParseText(code, new CSharpParseOptions(kind: SourceCodeKind.Script)); + InvocationExpressionSyntax setupInvocation = tree.GetRoot() + .DescendantNodes().OfType().First(); + + ExpressionSyntax? result = setupInvocation.FindMockedMemberExpressionFromSetupMethod(); + + Assert.Null(result); + } + + [Fact] + public async Task IsRaisesMethodCall_ValidRaisesCall_ReturnsTrue() + { + const string code = @" +using System; +using Moq; + +public interface IService +{ + event EventHandler MyEvent; + void DoWork(); +} + +public class C +{ + public void M() + { + var mock = new Mock(); + mock.Setup(x => x.DoWork()).Raises(x => x.MyEvent += null, EventArgs.Empty); + } +}"; + (SemanticModel model, SyntaxTree tree) = await CompilationHelper.CreateMoqCompilationAsync(code); + SyntaxNode root = await tree.GetRootAsync(); + MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); + + // Find the Raises invocation + InvocationExpressionSyntax raisesInvocation = root + .DescendantNodes().OfType() + .First(i => i.Expression is MemberAccessExpressionSyntax ma + && string.Equals(ma.Name.Identifier.Text, "Raises", StringComparison.Ordinal)); + + bool result = raisesInvocation.IsRaisesMethodCall(model, knownSymbols); + + Assert.True(result); + } + + [Fact] + public async Task IsRaisesMethodCall_NonRaisesMethod_ReturnsFalse() + { + const string code = @" +using System; +using Moq; + +public interface IService +{ + void DoWork(); +} + +public class C +{ + public void M() + { + var mock = new Mock(); + mock.Setup(x => x.DoWork()); + } +}"; + (SemanticModel model, SyntaxTree tree) = await CompilationHelper.CreateMoqCompilationAsync(code); + SyntaxNode root = await tree.GetRootAsync(); + MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); + + // Find the Setup invocation (not Raises) + InvocationExpressionSyntax setupInvocation = root + .DescendantNodes().OfType() + .First(i => i.Expression is MemberAccessExpressionSyntax ma + && string.Equals(ma.Name.Identifier.Text, "Setup", StringComparison.Ordinal)); + + bool result = setupInvocation.IsRaisesMethodCall(model, knownSymbols); + + Assert.False(result); + } + + [Fact] + public async Task IsRaisesMethodCall_ExpressionNotMemberAccess_ReturnsFalse() + { + // A direct method call (not member access) such as a local function call + const string code = @" +using System; +using Moq; + +public class C +{ + public void M() + { + DoSomething(); + } + + private static void DoSomething() { } +}"; + (SemanticModel model, SyntaxTree tree) = await CompilationHelper.CreateMoqCompilationAsync(code); + SyntaxNode root = await tree.GetRootAsync(); + MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); + + InvocationExpressionSyntax invocation = root + .DescendantNodes().OfType().First(); + + bool result = invocation.IsRaisesMethodCall(model, knownSymbols); + + Assert.False(result); + } +} diff --git a/tests/Moq.Analyzers.Test/Common/MoqKnownSymbolExtensionsTests.cs b/tests/Moq.Analyzers.Test/Common/MoqKnownSymbolExtensionsTests.cs new file mode 100644 index 000000000..fff1b5b52 --- /dev/null +++ b/tests/Moq.Analyzers.Test/Common/MoqKnownSymbolExtensionsTests.cs @@ -0,0 +1,35 @@ +using Moq.Analyzers.Common.WellKnown; +using Moq.Analyzers.Test.Helpers; + +namespace Moq.Analyzers.Test.Common; + +public class MoqKnownSymbolExtensionsTests +{ + [Fact] + public async Task IsMockReferenced_WithMoqCompilation_ReturnsTrue() + { + MoqKnownSymbols symbols = await CreateSymbolsWithMoqAsync(); + + Assert.True(symbols.IsMockReferenced()); + } + + [Fact] + public void IsMockReferenced_WithoutMoqCompilation_ReturnsFalse() + { + MoqKnownSymbols symbols = CreateSymbolsWithoutMoq(); + + Assert.False(symbols.IsMockReferenced()); + } + + private static MoqKnownSymbols CreateSymbolsWithoutMoq() + { + (SemanticModel model, _) = CompilationHelper.CreateCompilation("public class Empty { }"); + return new MoqKnownSymbols(model.Compilation); + } + + private static async Task CreateSymbolsWithMoqAsync() + { + (SemanticModel model, _) = await CompilationHelper.CreateMoqCompilationAsync("public class Empty { }").ConfigureAwait(false); + return new MoqKnownSymbols(model.Compilation); + } +} diff --git a/tests/Moq.Analyzers.Test/Common/MoqKnownSymbolsTests.cs b/tests/Moq.Analyzers.Test/Common/MoqKnownSymbolsTests.cs index ffbf461e0..b2afd5095 100644 --- a/tests/Moq.Analyzers.Test/Common/MoqKnownSymbolsTests.cs +++ b/tests/Moq.Analyzers.Test/Common/MoqKnownSymbolsTests.cs @@ -1,5 +1,3 @@ -using System.Collections.Immutable; -using Microsoft.CodeAnalysis.Testing; using Moq.Analyzers.Common.WellKnown; using Moq.Analyzers.Test.Helpers; @@ -7,24 +5,13 @@ namespace Moq.Analyzers.Test.Common; public class MoqKnownSymbolsTests { - private static readonly MetadataReference CorlibReference; - private static readonly MetadataReference SystemRuntimeReference; - private static readonly MetadataReference SystemThreadingTasksReference; - private static readonly MetadataReference SystemLinqReference; +#pragma warning disable ECS1300 // Static field init is simpler than static constructor for single field + private static readonly MetadataReference SystemThreadingTasksReference = + MetadataReference.CreateFromFile(typeof(System.Threading.Tasks.Task).Assembly.Location); +#pragma warning restore ECS1300 -#pragma warning disable S3963 // "static fields" should be initialized inline - conflicts with ECS1300 - static MoqKnownSymbolsTests() - { - CorlibReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); - string runtimeDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!; - SystemRuntimeReference = MetadataReference.CreateFromFile(Path.Combine(runtimeDir, "System.Runtime.dll")); - SystemThreadingTasksReference = MetadataReference.CreateFromFile(typeof(System.Threading.Tasks.Task).Assembly.Location); - SystemLinqReference = MetadataReference.CreateFromFile(typeof(System.Linq.Enumerable).Assembly.Location); - } -#pragma warning restore S3963 - - private static MetadataReference[] CoreReferences => - [CorlibReference, SystemRuntimeReference, SystemThreadingTasksReference, SystemLinqReference]; + private static MetadataReference[] CoreReferencesWithTasks => + [CompilationHelper.CorlibReference, CompilationHelper.SystemRuntimeReference, SystemThreadingTasksReference, CompilationHelper.SystemLinqReference]; [Fact] public void Constructor_WithCompilation_CreatesInstance() @@ -997,12 +984,8 @@ public async Task BothConstructors_ProduceSameResults_ForMock1Type() private static CSharpCompilation CreateMinimalCompilation() { - SyntaxTree tree = CSharpSyntaxTree.ParseText("public class Empty { }"); - return CSharpCompilation.Create( - "TestAssembly", - new[] { tree }, - CoreReferences, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + (SemanticModel model, _) = CompilationHelper.CreateCompilation("public class Empty { }", CoreReferencesWithTasks); + return (CSharpCompilation)model.Compilation; } private static MoqKnownSymbols CreateSymbolsWithoutMoq() @@ -1017,19 +1000,7 @@ private static async Task CreateSymbolsWithMoqAsync() private static async Task CreateMoqCompilationAsync() { - SyntaxTree tree = CSharpSyntaxTree.ParseText("public class Empty { }"); - MetadataReference[] references = await GetMoqReferencesAsync().ConfigureAwait(false); - return CSharpCompilation.Create( - "TestAssembly", - new[] { tree }, - references, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - } - - private static async Task GetMoqReferencesAsync() - { - ReferenceAssemblies referenceAssemblies = ReferenceAssemblyCatalog.Catalog[ReferenceAssemblyCatalog.Net80WithNewMoq]; - ImmutableArray resolved = await referenceAssemblies.ResolveAsync(LanguageNames.CSharp, CancellationToken.None).ConfigureAwait(false); - return [.. resolved]; + (SemanticModel model, _) = await CompilationHelper.CreateMoqCompilationAsync("public class Empty { }").ConfigureAwait(false); + return (CSharpCompilation)model.Compilation; } } diff --git a/tests/Moq.Analyzers.Test/Common/MoqVerificationHelpersTests.cs b/tests/Moq.Analyzers.Test/Common/MoqVerificationHelpersTests.cs index 4da0a8c36..5f264f904 100644 --- a/tests/Moq.Analyzers.Test/Common/MoqVerificationHelpersTests.cs +++ b/tests/Moq.Analyzers.Test/Common/MoqVerificationHelpersTests.cs @@ -8,12 +8,12 @@ public class MoqVerificationHelpersTests public void ExtractLambdaFromArgument_ReturnsLambda_ForDirectLambda() { string code = @"class C { void M() { System.Action a = () => { }; } }"; - SyntaxTree tree = CSharpSyntaxTree.ParseText(code); - CSharpCompilation compilation = CSharpCompilation.Create("Test", new[] { tree }, new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) }); - SemanticModel model = compilation.GetSemanticModel(tree); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); ParenthesizedLambdaExpressionSyntax lambda = tree.GetRoot().DescendantNodes().OfType().First(); IOperation? operation = model.GetOperation(lambda); + IAnonymousFunctionOperation? result = MoqVerificationHelpers.ExtractLambdaFromArgument(operation!); + Assert.NotNull(result); Assert.IsAssignableFrom(result); } @@ -22,12 +22,12 @@ public void ExtractLambdaFromArgument_ReturnsLambda_ForDirectLambda() public void ExtractLambdaFromArgument_ReturnsLambda_ForSimpleLambda() { string code = @"class C { void M() { System.Func f = x => x.ToString(); } }"; - SyntaxTree tree = CSharpSyntaxTree.ParseText(code); - CSharpCompilation compilation = CSharpCompilation.Create("Test", new[] { tree }, new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) }); - SemanticModel model = compilation.GetSemanticModel(tree); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); SimpleLambdaExpressionSyntax lambda = tree.GetRoot().DescendantNodes().OfType().First(); IOperation? operation = model.GetOperation(lambda); + IAnonymousFunctionOperation? result = MoqVerificationHelpers.ExtractLambdaFromArgument(operation!); + Assert.NotNull(result); Assert.IsAssignableFrom(result); } @@ -36,22 +36,13 @@ public void ExtractLambdaFromArgument_ReturnsLambda_ForSimpleLambda() public void ExtractLambdaFromArgument_ReturnsNull_ForMethodGroupConversion() { string code = @"class C { void M() { System.Action a = M2; } void M2() { } }"; - SyntaxTree tree = CSharpSyntaxTree.ParseText(code); - CSharpCompilation compilation = CSharpCompilation.Create("Test", new[] { tree }, new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) }); - SemanticModel model = compilation.GetSemanticModel(tree); - IdentifierNameSyntax methodGroup = tree.GetRoot().DescendantNodes().OfType().First(id => string.Equals(id.Identifier.Text, "M2", StringComparison.Ordinal)); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); + IdentifierNameSyntax methodGroup = tree.GetRoot().DescendantNodes().OfType() + .First(id => string.Equals(id.Identifier.Text, "M2", StringComparison.Ordinal)); IOperation? operation = model.GetOperation(methodGroup); + IAnonymousFunctionOperation? result = MoqVerificationHelpers.ExtractLambdaFromArgument(operation!); - Assert.Null(result); - } - [Fact] - public void ExtractLambdaFromArgument_ReturnsNull_ForNullInput() - { - // Suppress CS8625: Cannot convert null literal to non-nullable reference type -#pragma warning disable CS8625 - IAnonymousFunctionOperation? result = MoqVerificationHelpers.ExtractLambdaFromArgument(null); -#pragma warning restore CS8625 Assert.Null(result); } @@ -59,14 +50,12 @@ public void ExtractLambdaFromArgument_ReturnsNull_ForNullInput() public void ExtractLambdaFromArgument_ReturnsLambda_ForNestedLambda() { string code = @"class C { void M() { System.Func f = x => () => { }; } }"; - SyntaxTree tree = CSharpSyntaxTree.ParseText(code); - CSharpCompilation compilation = CSharpCompilation.Create("Test", new[] { tree }, new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) }); - SemanticModel model = compilation.GetSemanticModel(tree); - - // Find the inner (nested) lambda + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); ParenthesizedLambdaExpressionSyntax innerLambda = tree.GetRoot().DescendantNodes().OfType().First(); IOperation? operation = model.GetOperation(innerLambda); + IAnonymousFunctionOperation? result = MoqVerificationHelpers.ExtractLambdaFromArgument(operation!); + Assert.NotNull(result); Assert.IsAssignableFrom(result); } @@ -75,12 +64,311 @@ public void ExtractLambdaFromArgument_ReturnsLambda_ForNestedLambda() public void ExtractLambdaFromArgument_ReturnsNull_ForNonLambda() { string code = @"class C { void M() { int x = 1; } }"; - SyntaxTree tree = CSharpSyntaxTree.ParseText(code); - CSharpCompilation compilation = CSharpCompilation.Create("Test", new[] { tree }, new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) }); - SemanticModel model = compilation.GetSemanticModel(tree); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); LiteralExpressionSyntax literal = tree.GetRoot().DescendantNodes().OfType().First(); IOperation? operation = model.GetOperation(literal); + IAnonymousFunctionOperation? result = MoqVerificationHelpers.ExtractLambdaFromArgument(operation!); + + Assert.Null(result); + } + + [Fact] + public async Task ExtractPropertyFromVerifySetLambda_AssignmentWithPropertyRef_ReturnsProperty() + { + const string code = @" +using Moq; + +public interface IFoo +{ + int Value { get; set; } +} + +public class Test +{ + public void M() + { + var mock = new Mock(); + mock.VerifySet(x => x.Value = 42); + } +}"; + IInvocationOperation invocation = await GetMoqInvocationAsync(code, "VerifySet"); + IAnonymousFunctionOperation? lambda = MoqVerificationHelpers.ExtractLambdaFromArgument(invocation.Arguments[0].Value); + Assert.NotNull(lambda); + + ISymbol? result = MoqVerificationHelpers.ExtractPropertyFromVerifySetLambda(lambda!); + + Assert.NotNull(result); + Assert.IsAssignableFrom(result); + Assert.Equal("Value", result!.Name); + } + + [Fact] + public async Task ExtractPropertyFromVerifySetLambda_NonAssignmentExpression_ReturnsNull() + { + // Use a lambda whose body contains an ExpressionStatementOperation that is NOT an assignment. + // Calling a method on the mock parameter produces an IExpressionStatementOperation wrapping + // an IInvocationOperation (not an IAssignmentOperation). + const string code = @" +using Moq; + +public interface IFoo +{ + void DoSomething(); +} + +public class Test +{ + public void M() + { + var mock = new Mock(); + mock.VerifySet(x => { x.DoSomething(); }); + } +}"; + IInvocationOperation invocation = await GetMoqInvocationAsync(code, "VerifySet"); + IAnonymousFunctionOperation? lambda = MoqVerificationHelpers.ExtractLambdaFromArgument(invocation.Arguments[0].Value); + Assert.NotNull(lambda); + + ISymbol? result = MoqVerificationHelpers.ExtractPropertyFromVerifySetLambda(lambda!); + + Assert.Null(result); + } + + [Fact] + public async Task ExtractPropertyFromVerifySetLambda_EmptyBody_ReturnsNull() + { + // A lambda with an empty block body { } still has a body with zero user operations + // (only an implicit return). The method iterates Body.Operations and finds nothing. + const string code = @" +using Moq; + +public interface IFoo +{ + int Value { get; set; } +} + +public class Test +{ + public void M() + { + var mock = new Mock(); + mock.VerifySet(x => { }); + } +}"; + IInvocationOperation invocation = await GetMoqInvocationAsync(code, "VerifySet"); + IAnonymousFunctionOperation? lambda = MoqVerificationHelpers.ExtractLambdaFromArgument(invocation.Arguments[0].Value); + Assert.NotNull(lambda); + + ISymbol? result = MoqVerificationHelpers.ExtractPropertyFromVerifySetLambda(lambda!); + + Assert.Null(result); + } + + [Fact] + public async Task TryGetMockedMemberSymbol_ZeroArguments_ReturnsNull() + { + // Verify() with no arguments produces an IInvocationOperation with 0 args. + const string code = @" +using Moq; + +public interface IFoo +{ + void DoSomething(); +} + +public class Test +{ + public void M() + { + var mock = new Mock(); + mock.Verify(); + } +}"; + IInvocationOperation invocation = await GetMoqInvocationAsync(code, "Verify"); + + ISymbol? result = MoqVerificationHelpers.TryGetMockedMemberSymbol(invocation); + + Assert.Null(result); + } + + [Fact] + public async Task TryGetMockedMemberSymbol_LambdaWithMethodInvocation_ReturnsMethodSymbol() + { + const string code = @" +using Moq; + +public interface IFoo +{ + int DoSomething(); +} + +public class Test +{ + public void M() + { + var mock = new Mock(); + mock.Setup(x => x.DoSomething()); + } +}"; + IInvocationOperation invocation = await GetMoqInvocationAsync(code, "Setup"); + + ISymbol? result = MoqVerificationHelpers.TryGetMockedMemberSymbol(invocation); + + Assert.NotNull(result); + Assert.IsAssignableFrom(result); + Assert.Equal("DoSomething", result!.Name); + } + + [Fact] + public async Task TryGetMockedMemberSymbol_LambdaWithPropertyAccess_ReturnsPropertySymbol() + { + const string code = @" +using Moq; + +public interface IFoo +{ + int Value { get; } +} + +public class Test +{ + public void M() + { + var mock = new Mock(); + mock.Setup(x => x.Value); + } +}"; + IInvocationOperation invocation = await GetMoqInvocationAsync(code, "Setup"); + + ISymbol? result = MoqVerificationHelpers.TryGetMockedMemberSymbol(invocation); + + Assert.NotNull(result); + Assert.IsAssignableFrom(result); + Assert.Equal("Value", result!.Name); + } + + [Fact] + public async Task TryGetMockedMemberSyntax_ZeroArguments_ReturnsNull() + { + const string code = @" +using Moq; + +public interface IFoo +{ + void DoSomething(); +} + +public class Test +{ + public void M() + { + var mock = new Mock(); + mock.Verify(); + } +}"; + IInvocationOperation invocation = await GetMoqInvocationAsync(code, "Verify"); + + SyntaxNode? result = MoqVerificationHelpers.TryGetMockedMemberSyntax(invocation); + + Assert.Null(result); + } + + [Fact] + public async Task TryGetMockedMemberSyntax_LambdaWithMethodInvocation_ReturnsSyntaxNode() + { + const string code = @" +using Moq; + +public interface IFoo +{ + int DoSomething(); +} + +public class Test +{ + public void M() + { + var mock = new Mock(); + mock.Setup(x => x.DoSomething()); + } +}"; + IInvocationOperation invocation = await GetMoqInvocationAsync(code, "Setup"); + + SyntaxNode? result = MoqVerificationHelpers.TryGetMockedMemberSyntax(invocation); + + Assert.NotNull(result); + Assert.Contains("DoSomething", result!.ToString(), StringComparison.Ordinal); + } + + [Fact] + public void TryGetMockedMemberSyntax_NonLambdaArgument_ReturnsNull() + { + // When the first argument is not a lambda, ExtractLambdaFromArgument returns null, + // causing TryGetMockedMemberSyntax to return null via null propagation. + const string code = @" +class C +{ + static void Call(object arg) { } + void M() + { + Call(42); + } +}"; + IInvocationOperation invocation = GetInvocationOperation(code, "Call"); + + SyntaxNode? result = MoqVerificationHelpers.TryGetMockedMemberSyntax(invocation); + Assert.Null(result); } + + [Fact] + public void TryGetMockedMemberSymbol_NonLambdaArgument_ReturnsNull() + { + // When the first argument is not a lambda, ExtractLambdaFromArgument returns null. + const string code = @" +class C +{ + static void Call(object arg) { } + void M() + { + Call(42); + } +}"; + IInvocationOperation invocation = GetInvocationOperation(code, "Call"); + + ISymbol? result = MoqVerificationHelpers.TryGetMockedMemberSymbol(invocation); + + Assert.Null(result); + } + + private static IInvocationOperation GetInvocationOperation(string code, string methodName) + { + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); + InvocationExpressionSyntax invocationSyntax = tree.GetRoot() + .DescendantNodes() + .OfType() + .First(inv => inv.Expression.ToString().Contains(methodName, StringComparison.Ordinal)); + IOperation? operation = model.GetOperation(invocationSyntax); + Assert.NotNull(operation); + Assert.IsAssignableFrom(operation); + return (IInvocationOperation)operation!; + } + + private static async Task GetMoqInvocationAsync(string code, string methodName) + { + (SemanticModel model, SyntaxTree tree) = await CompilationHelper.CreateMoqCompilationAsync(code).ConfigureAwait(false); + SyntaxNode root = await tree.GetRootAsync().ConfigureAwait(false); + InvocationExpressionSyntax invocationSyntax = root + .DescendantNodes() + .OfType() + .First(inv => + { + string text = inv.Expression.ToString(); + return text.Contains(methodName, StringComparison.Ordinal); + }); + IOperation? operation = model.GetOperation(invocationSyntax); + Assert.NotNull(operation); + Assert.IsAssignableFrom(operation); + return (IInvocationOperation)operation!; + } } diff --git a/tests/Moq.Analyzers.Test/Common/SemanticModelExtensionsTests.cs b/tests/Moq.Analyzers.Test/Common/SemanticModelExtensionsTests.cs index 86a8685f0..11fd59c67 100644 --- a/tests/Moq.Analyzers.Test/Common/SemanticModelExtensionsTests.cs +++ b/tests/Moq.Analyzers.Test/Common/SemanticModelExtensionsTests.cs @@ -1,4 +1,6 @@ using Microsoft.CodeAnalysis.CSharp; +using Moq.Analyzers.Common.WellKnown; +using Moq.Analyzers.Test.Helpers; namespace Moq.Analyzers.Test.Common; @@ -7,25 +9,6 @@ public class SemanticModelExtensionsTests // Methods requiring Moq compilation reference (FindSetupMethodFromCallbackInvocation, // GetAllMatchingMockedMethodSymbolsFromSetupMethodInvocation, IsCallbackOrReturnInvocation, // IsRaisesInvocation) require Moq symbols and are covered by integration tests. - private static readonly MetadataReference CorlibReference; - private static readonly MetadataReference SystemRuntimeReference; - private static readonly MetadataReference SystemLinqReference; - private static readonly MetadataReference SystemLinqExpressionsReference; - -#pragma warning disable S3963 // "static fields" should be initialized inline - conflicts with ECS1300 - static SemanticModelExtensionsTests() - { - CorlibReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); - string runtimeDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!; - SystemRuntimeReference = MetadataReference.CreateFromFile(Path.Combine(runtimeDir, "System.Runtime.dll")); - SystemLinqReference = MetadataReference.CreateFromFile(typeof(System.Linq.Enumerable).Assembly.Location); - SystemLinqExpressionsReference = MetadataReference.CreateFromFile(typeof(System.Linq.Expressions.Expression).Assembly.Location); - } -#pragma warning restore S3963 - - private static MetadataReference[] CoreReferences => - [CorlibReference, SystemRuntimeReference, SystemLinqReference, SystemLinqExpressionsReference]; - [Fact] public void HasConversion_IntToLong_ImplicitConversion_ReturnsTrue() { @@ -237,7 +220,7 @@ void M() int x = 42; } }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CompilationHelper.CoreReferencesWithLinq); LiteralExpressionSyntax literal = tree.GetRoot() .DescendantNodes() @@ -370,7 +353,7 @@ void M() int x = 42; } }"; - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CompilationHelper.CoreReferencesWithLinq); LiteralExpressionSyntax literal = tree.GetRoot() .DescendantNodes() @@ -452,16 +435,320 @@ void M() Assert.Null(eventType); } - private static (SemanticModel Model, SyntaxTree Tree) CreateCompilation(string code) + [Fact] + public async Task FindSetupMethodFromCallbackInvocation_ValidReturnsChain_FindsSetupInvocation() + { + const string code = @" +using Moq; +public interface IFoo { int Bar(); } +public class C +{ + public void M() + { + var mock = new Mock(); + mock.Setup(x => x.Bar()).Returns(42); + } +}"; + (SemanticModel model, SyntaxTree tree) = await CompilationHelper.CreateMoqCompilationAsync(code); + MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); + SyntaxNode root = await tree.GetRootAsync(); + InvocationExpressionSyntax returnsInvocation = root + .DescendantNodes().OfType() + .First(i => i.Expression is MemberAccessExpressionSyntax ma + && string.Equals(ma.Name.Identifier.Text, "Returns", StringComparison.Ordinal)); + + InvocationExpressionSyntax? setupInvocation = model.FindSetupMethodFromCallbackInvocation( + knownSymbols, returnsInvocation, CancellationToken.None); + + Assert.NotNull(setupInvocation); + MemberAccessExpressionSyntax setupAccess = (MemberAccessExpressionSyntax)setupInvocation!.Expression; + Assert.Equal("Setup", setupAccess.Name.Identifier.Text); + } + + [Fact] + public async Task FindSetupMethodFromCallbackInvocation_ExpressionNotInvocation_ReturnsNull() + { + const string code = @" +using Moq; +public interface IFoo { void Bar(); } +public class C +{ + public void M() + { + var mock = new Mock(); + mock.Setup(x => x.Bar()); + } +}"; + (SemanticModel model, _) = await CompilationHelper.CreateMoqCompilationAsync(code); + MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); + + // Pass a non-invocation expression (a literal) + LiteralExpressionSyntax literal = SyntaxFactory.LiteralExpression( + SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(42)); + + InvocationExpressionSyntax? result = model.FindSetupMethodFromCallbackInvocation( + knownSymbols, literal, CancellationToken.None); + + Assert.Null(result); + } + + [Fact] + public async Task GetAllMatchingMockedMethodSymbolsFromSetupMethodInvocation_ValidSetup_ReturnsMethodSymbols() + { + const string code = @" +using Moq; +public interface IFoo { int Bar(); } +public class C +{ + public void M() { - SyntaxTree tree = CSharpSyntaxTree.ParseText(code); - CSharpCompilation compilation = CSharpCompilation.Create( - "TestAssembly", - new[] { tree }, - CoreReferences, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - SemanticModel model = compilation.GetSemanticModel(tree); - return (model, tree); + var mock = new Mock(); + mock.Setup(x => x.Bar()).Returns(42); + } +}"; + (SemanticModel model, SyntaxTree tree) = await CompilationHelper.CreateMoqCompilationAsync(code); + SyntaxNode root = await tree.GetRootAsync(); + InvocationExpressionSyntax setupInvocation = root + .DescendantNodes().OfType() + .First(i => i.Expression is MemberAccessExpressionSyntax ma + && string.Equals(ma.Name.Identifier.Text, "Setup", StringComparison.Ordinal)); + + IEnumerable symbols = model.GetAllMatchingMockedMethodSymbolsFromSetupMethodInvocation(setupInvocation); + + Assert.NotEmpty(symbols); + Assert.Contains(symbols, s => string.Equals(s.Name, "Bar", StringComparison.Ordinal)); + } + + [Fact] + public async Task GetAllMatchingMockedMethodSymbolsFromSetupMethodInvocation_NullInput_ReturnsEmpty() + { + const string code = @" +using Moq; +public class C { }"; + (SemanticModel model, _) = await CompilationHelper.CreateMoqCompilationAsync(code); + + IEnumerable symbols = model.GetAllMatchingMockedMethodSymbolsFromSetupMethodInvocation(null); + + Assert.Empty(symbols); + } + + [Fact] + public async Task GetAllMatchingMockedMethodSymbolsFromSetupMethodInvocation_LambdaBodyNotInvocation_ReturnsEmpty() + { + // Create a setup where the lambda body is a property access, not a method invocation + const string code = @" +using Moq; +public interface IFoo { int Value { get; } } +public class C +{ + public void M() + { + var mock = new Mock(); + mock.Setup(x => x.Value); + } +}"; + (SemanticModel model, SyntaxTree tree) = await CompilationHelper.CreateMoqCompilationAsync(code); + SyntaxNode root = await tree.GetRootAsync(); + InvocationExpressionSyntax setupInvocation = root + .DescendantNodes().OfType() + .First(i => i.Expression is MemberAccessExpressionSyntax ma + && string.Equals(ma.Name.Identifier.Text, "Setup", StringComparison.Ordinal)); + + IEnumerable symbols = model.GetAllMatchingMockedMethodSymbolsFromSetupMethodInvocation(setupInvocation); + + Assert.Empty(symbols); + } + + [Fact] + public async Task IsCallbackOrReturnInvocation_CallbackInvocation_ReturnsTrue() + { + const string code = @" +using Moq; +public interface IFoo { void Bar(); } +public class C +{ + public void M() + { + var mock = new Mock(); + mock.Setup(x => x.Bar()).Callback(() => { }); + } +}"; + (SemanticModel model, SyntaxTree tree) = await CompilationHelper.CreateMoqCompilationAsync(code); + MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); + SyntaxNode root = await tree.GetRootAsync(); + InvocationExpressionSyntax callbackInvocation = root + .DescendantNodes().OfType() + .First(i => i.Expression is MemberAccessExpressionSyntax ma + && string.Equals(ma.Name.Identifier.Text, "Callback", StringComparison.Ordinal)); + + bool result = model.IsCallbackOrReturnInvocation(callbackInvocation, knownSymbols); + + Assert.True(result); + } + + [Fact] + public async Task IsCallbackOrReturnInvocation_ReturnsInvocation_ReturnsTrue() + { + const string code = @" +using Moq; +public interface IFoo { int Bar(); } +public class C +{ + public void M() + { + var mock = new Mock(); + mock.Setup(x => x.Bar()).Returns(42); + } +}"; + (SemanticModel model, SyntaxTree tree) = await CompilationHelper.CreateMoqCompilationAsync(code); + MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); + SyntaxNode root = await tree.GetRootAsync(); + InvocationExpressionSyntax returnsInvocation = root + .DescendantNodes().OfType() + .First(i => i.Expression is MemberAccessExpressionSyntax ma + && string.Equals(ma.Name.Identifier.Text, "Returns", StringComparison.Ordinal)); + + bool result = model.IsCallbackOrReturnInvocation(returnsInvocation, knownSymbols); + + Assert.True(result); + } + + [Fact] + public async Task IsCallbackOrReturnInvocation_NonCallbackReturnsMethodName_ReturnsFalse() + { + const string code = @" +using Moq; +public interface IFoo { int Bar(); } +public class C +{ + public void M() + { + var mock = new Mock(); + mock.Setup(x => x.Bar()); + } +}"; + (SemanticModel model, SyntaxTree tree) = await CompilationHelper.CreateMoqCompilationAsync(code); + MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); + SyntaxNode root = await tree.GetRootAsync(); + InvocationExpressionSyntax setupInvocation = root + .DescendantNodes().OfType() + .First(i => i.Expression is MemberAccessExpressionSyntax ma + && string.Equals(ma.Name.Identifier.Text, "Setup", StringComparison.Ordinal)); + + bool result = model.IsCallbackOrReturnInvocation(setupInvocation, knownSymbols); + + Assert.False(result); + } + + [Fact] + public async Task IsCallbackOrReturnInvocation_ExpressionNotMemberAccess_ReturnsFalse() + { + // Create an invocation that does not use member access (direct method call) + const string code = @" +using Moq; +public class C +{ + static void Foo() { } + public void M() + { + Foo(); + } +}"; + (SemanticModel model, SyntaxTree tree) = await CompilationHelper.CreateMoqCompilationAsync(code); + MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); + SyntaxNode root = await tree.GetRootAsync(); + InvocationExpressionSyntax fooInvocation = root + .DescendantNodes().OfType() + .First(i => i.Expression is IdentifierNameSyntax id + && string.Equals(id.Identifier.Text, "Foo", StringComparison.Ordinal)); + + bool result = model.IsCallbackOrReturnInvocation(fooInvocation, knownSymbols); + + Assert.False(result); + } + + [Fact] + public async Task IsRaisesInvocation_RaisesCall_ReturnsTrue() + { + const string code = @" +using Moq; +using System; +public interface IFoo { event EventHandler MyEvent; void Bar(); } +public class C +{ + public void M() + { + var mock = new Mock(); + mock.Setup(x => x.Bar()).Raises(x => x.MyEvent += null, EventArgs.Empty); + } +}"; + (SemanticModel model, SyntaxTree tree) = await CompilationHelper.CreateMoqCompilationAsync(code); + MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); + SyntaxNode root = await tree.GetRootAsync(); + InvocationExpressionSyntax raisesInvocation = root + .DescendantNodes().OfType() + .First(i => i.Expression is MemberAccessExpressionSyntax ma + && string.Equals(ma.Name.Identifier.Text, "Raises", StringComparison.Ordinal)); + + bool result = model.IsRaisesInvocation(raisesInvocation, knownSymbols); + + Assert.True(result); + } + + [Fact] + public async Task IsRaisesInvocation_ExpressionNotMemberAccess_ReturnsFalse() + { + const string code = @" +using Moq; +public class C +{ + static void Raises() { } + public void M() + { + Raises(); + } +}"; + (SemanticModel model, SyntaxTree tree) = await CompilationHelper.CreateMoqCompilationAsync(code); + MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); + SyntaxNode root = await tree.GetRootAsync(); + InvocationExpressionSyntax raisesInvocation = root + .DescendantNodes().OfType() + .First(i => i.Expression is IdentifierNameSyntax id + && string.Equals(id.Identifier.Text, "Raises", StringComparison.Ordinal)); + + bool result = model.IsRaisesInvocation(raisesInvocation, knownSymbols); + + Assert.False(result); + } + + [Fact] + public async Task IsRaisesInvocation_NonMoqMethodNamedRaises_ReturnsFalse() + { + const string code = @" +using Moq; +public class MyClass +{ + public MyClass Raises() => this; +} +public class C +{ + public void M() + { + var obj = new MyClass(); + obj.Raises(); + } +}"; + (SemanticModel model, SyntaxTree tree) = await CompilationHelper.CreateMoqCompilationAsync(code); + MoqKnownSymbols knownSymbols = new MoqKnownSymbols(model.Compilation); + SyntaxNode root = await tree.GetRootAsync(); + InvocationExpressionSyntax raisesInvocation = root + .DescendantNodes().OfType() + .First(i => i.Expression is MemberAccessExpressionSyntax ma + && string.Equals(ma.Name.Identifier.Text, "Raises", StringComparison.Ordinal)); + + bool result = model.IsRaisesInvocation(raisesInvocation, knownSymbols); + + Assert.False(result); } private static (SemanticModel Model, ITypeSymbol FirstType, ITypeSymbol SecondType) GetTwoVariableTypes( @@ -469,7 +756,7 @@ private static (SemanticModel Model, ITypeSymbol FirstType, ITypeSymbol SecondTy string firstName, string secondName) { - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CompilationHelper.CoreReferencesWithLinq); VariableDeclaratorSyntax[] declarators = tree.GetRoot() .DescendantNodes() .OfType() @@ -487,7 +774,7 @@ private static (SemanticModel Model, ExpressionSyntax Lambda) GetLambdaFromVaria string code, string variableName) { - (SemanticModel model, SyntaxTree tree) = CreateCompilation(code); + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code, CompilationHelper.CoreReferencesWithLinq); VariableDeclaratorSyntax declarator = tree.GetRoot() .DescendantNodes() .OfType() diff --git a/tests/Moq.Analyzers.Test/Common/SyntaxNodeExtensionsTests.cs b/tests/Moq.Analyzers.Test/Common/SyntaxNodeExtensionsTests.cs new file mode 100644 index 000000000..779ba3e91 --- /dev/null +++ b/tests/Moq.Analyzers.Test/Common/SyntaxNodeExtensionsTests.cs @@ -0,0 +1,177 @@ +namespace Moq.Analyzers.Test.Common; + +public class SyntaxNodeExtensionsTests +{ + [Fact] + public void FindLocation_MatchingDescendantFound_ReturnsLocationWithCorrectSpan() + { + const string code = @" +class C +{ + void M() + { + var x = new C(); + } + static C Create() => new C(); +}"; + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); + SyntaxNode root = tree.GetRoot(); + + // Get the constructor symbol from the first ObjectCreationExpressionSyntax + ObjectCreationExpressionSyntax creation = root + .DescendantNodes().OfType().First(); + ISymbol? ctorSymbol = model.GetSymbolInfo(creation).Symbol; + Assert.NotNull(ctorSymbol); + + // FindLocation should find a descendant ObjectCreationExpressionSyntax matching the ctor symbol + Location? location = root.FindLocation(ctorSymbol, model); + + Assert.NotNull(location); + Assert.Equal(creation.Span, location.SourceSpan); + } + + [Fact] + public void FindLocation_NoMatchingDescendant_ReturnsNull() + { + const string code = @" +class C +{ + void M() { } +} +class D +{ + D() { } +}"; + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); + SyntaxNode root = tree.GetRoot(); + + // Get the D constructor symbol + ClassDeclarationSyntax classD = root + .DescendantNodes().OfType() + .First(c => string.Equals(c.Identifier.Text, "D", StringComparison.Ordinal)); + INamedTypeSymbol? typeDSymbol = model.GetDeclaredSymbol(classD); + Assert.NotNull(typeDSymbol); + IMethodSymbol ctorSymbol = typeDSymbol.InstanceConstructors.First(); + + // Search within class C only (which has no ObjectCreationExpressionSyntax for D) + ClassDeclarationSyntax classC = root + .DescendantNodes().OfType() + .First(c => string.Equals(c.Identifier.Text, "C", StringComparison.Ordinal)); + + Location? location = classC.FindLocation(ctorSymbol, model); + + Assert.Null(location); + } + + [Fact] + public void FindLocation_SemanticModelIsNull_ReturnsNull() + { + const string code = @" +class C +{ + void M() + { + var x = new C(); + } +}"; + (SemanticModel model, SyntaxTree tree) = CompilationHelper.CreateCompilation(code); + SyntaxNode root = tree.GetRoot(); + + // Get a real symbol to pass in + ObjectCreationExpressionSyntax creation = root + .DescendantNodes().OfType().First(); + ISymbol? ctorSymbol = model.GetSymbolInfo(creation).Symbol; + Assert.NotNull(ctorSymbol); + + // Call with null semantic model + Location? location = root.FindLocation(ctorSymbol, null); + + Assert.Null(location); + } + + [Fact] + public void GetParentSkippingParentheses_NoParenthesizedParent_ReturnsDirectParent() + { + // In `1 + 2`, the LiteralExpressionSyntax "1" has BinaryExpressionSyntax as parent + SyntaxTree tree = CSharpSyntaxTree.ParseText(@" +class C +{ + int M() => 1 + 2; +}"); + SyntaxNode root = tree.GetRoot(); + LiteralExpressionSyntax literal = root + .DescendantNodes().OfType() + .First(l => string.Equals(l.Token.ValueText, "1", StringComparison.Ordinal)); + + SyntaxNode? parent = literal.GetParentSkippingParentheses(); + + Assert.NotNull(parent); + Assert.IsType(parent); + Assert.Same(literal.Parent, parent); + } + + [Fact] + public void GetParentSkippingParentheses_OneLevelOfParentheses_SkipsToGrandparent() + { + // In `var x = (1 + 2)`, the BinaryExpression `1 + 2` parent is ParenthesizedExpression + SyntaxTree tree = CSharpSyntaxTree.ParseText(@" +class C +{ + void M() + { + var x = (1 + 2); + } +}"); + SyntaxNode root = tree.GetRoot(); + BinaryExpressionSyntax binary = root + .DescendantNodes().OfType().First(); + + // Direct parent should be ParenthesizedExpressionSyntax + Assert.IsType(binary.Parent); + + SyntaxNode? parent = binary.GetParentSkippingParentheses(); + + Assert.NotNull(parent); + Assert.IsNotType(parent); + Assert.IsType(parent); + } + + [Fact] + public void GetParentSkippingParentheses_MultipleLevelsOfParentheses_SkipsAll() + { + // In `var x = ((1 + 2))`, the BinaryExpression is wrapped in two levels of parens + SyntaxTree tree = CSharpSyntaxTree.ParseText(@" +class C +{ + void M() + { + var x = ((1 + 2)); + } +}"); + SyntaxNode root = tree.GetRoot(); + BinaryExpressionSyntax binary = root + .DescendantNodes().OfType().First(); + + // Verify nested parentheses exist + Assert.IsType(binary.Parent); + Assert.IsType(binary.Parent!.Parent); + + SyntaxNode? parent = binary.GetParentSkippingParentheses(); + + Assert.NotNull(parent); + Assert.IsNotType(parent); + Assert.IsType(parent); + } + + [Fact] + public void GetParentSkippingParentheses_NoParent_ReturnsNull() + { + // A CompilationUnit (root node) has no parent + SyntaxTree tree = CSharpSyntaxTree.ParseText("class C { }"); + SyntaxNode root = tree.GetRoot(); + + SyntaxNode? parent = root.GetParentSkippingParentheses(); + + Assert.Null(parent); + } +} diff --git a/tests/Moq.Analyzers.Test/Helpers/CompilationHelper.cs b/tests/Moq.Analyzers.Test/Helpers/CompilationHelper.cs new file mode 100644 index 000000000..4d89c806f --- /dev/null +++ b/tests/Moq.Analyzers.Test/Helpers/CompilationHelper.cs @@ -0,0 +1,91 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.Testing; + +namespace Moq.Analyzers.Test.Helpers; + +/// +/// Shared compilation boilerplate for unit tests that need Roslyn +/// and instances. +/// Eliminates per-class duplication of reference resolution and compilation creation. +/// +internal static class CompilationHelper +{ + internal static readonly MetadataReference CorlibReference; + internal static readonly MetadataReference SystemRuntimeReference; + internal static readonly MetadataReference SystemLinqReference; + internal static readonly MetadataReference SystemLinqExpressionsReference; + +#pragma warning disable S3963 // "static fields" should be initialized inline - multiple fields depend on runtimeDir + static CompilationHelper() + { + CorlibReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); + string runtimeDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!; + SystemRuntimeReference = MetadataReference.CreateFromFile(Path.Combine(runtimeDir, "System.Runtime.dll")); + SystemLinqReference = MetadataReference.CreateFromFile(typeof(System.Linq.Enumerable).Assembly.Location); + SystemLinqExpressionsReference = MetadataReference.CreateFromFile(typeof(System.Linq.Expressions.Expression).Assembly.Location); + } +#pragma warning restore S3963 + + /// + /// Gets the minimal set of references needed for most compilations: corlib + System.Runtime. + /// + internal static MetadataReference[] CoreReferences => [CorlibReference, SystemRuntimeReference]; + + /// + /// Gets core references plus System.Linq and System.Linq.Expressions. + /// Used by tests that compile code containing LINQ or expression trees. + /// + internal static MetadataReference[] CoreReferencesWithLinq => + [CorlibReference, SystemRuntimeReference, SystemLinqReference, SystemLinqExpressionsReference]; + + /// + /// Creates a from source code and returns the semantic model and syntax tree. + /// + /// C# source code to compile. + /// + /// Metadata references to use. Defaults to when null. + /// + /// A tuple of the and . + internal static (SemanticModel Model, SyntaxTree Tree) CreateCompilation( + string code, + MetadataReference[]? references = null) + { + SyntaxTree tree = CSharpSyntaxTree.ParseText(code); + CSharpCompilation compilation = CSharpCompilation.Create( + "TestAssembly", + new[] { tree }, + references ?? CoreReferences, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + SemanticModel model = compilation.GetSemanticModel(tree); + return (model, tree); + } + + /// + /// Creates a compilation that includes Moq package references resolved via NuGet. + /// + /// C# source code to compile. + /// A tuple of the and . + internal static async Task<(SemanticModel Model, SyntaxTree Tree)> CreateMoqCompilationAsync(string code) + { + SyntaxTree tree = CSharpSyntaxTree.ParseText(code); + MetadataReference[] references = await GetMoqReferencesAsync().ConfigureAwait(false); + CSharpCompilation compilation = CSharpCompilation.Create( + "TestAssembly", + new[] { tree }, + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + SemanticModel model = compilation.GetSemanticModel(tree); + return (model, tree); + } + + /// + /// Resolves Moq NuGet package references for use in test compilations. + /// + /// An array of including Moq and its dependencies. + internal static async Task GetMoqReferencesAsync() + { + ReferenceAssemblies referenceAssemblies = ReferenceAssemblyCatalog.Catalog[ReferenceAssemblyCatalog.Net80WithNewMoq]; + ImmutableArray resolved = await referenceAssemblies.ResolveAsync(LanguageNames.CSharp, CancellationToken.None).ConfigureAwait(false); + return [.. resolved]; + } +}