diff --git a/src/ErrorProne.NET.Cli/ErrorProne.NET.Cli.csproj b/src/ErrorProne.NET.Cli/ErrorProne.NET.Cli.csproj index ef75841..575524c 100644 --- a/src/ErrorProne.NET.Cli/ErrorProne.NET.Cli.csproj +++ b/src/ErrorProne.NET.Cli/ErrorProne.NET.Cli.csproj @@ -3,7 +3,7 @@ Exe - net46 + net472 true @@ -13,8 +13,8 @@ - - + + diff --git a/src/ErrorProne.NET.Core/EnumerableExtensions.cs b/src/ErrorProne.NET.Core/EnumerableExtensions.cs new file mode 100644 index 0000000..76e7c83 --- /dev/null +++ b/src/ErrorProne.NET.Core/EnumerableExtensions.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; + +namespace ErrorProne.NET.Core +{ + public static class EnumerableExtensions + { + public static T MinByOrDefault(this IEnumerable items, Func keySelector, IComparer keyComparer = null) + { + keyComparer = keyComparer ?? Comparer.Default; + T maxItem = default; + TKey maxKey = default; + bool isFirst = true; + + foreach (var item in items) + { + var currentKey = keySelector(item); + if (isFirst || keyComparer.Compare(currentKey, maxKey) < 0) + { + isFirst = false; + maxItem = item; + maxKey = currentKey; + } + } + + return maxItem; + } + + public static Dictionary> GroupToDictionary(this IEnumerable<(TKey, TValue)> sequence) + { + Dictionary> result = new Dictionary>(); + + foreach (var (key, value) in sequence) + { + if (result.TryGetValue(key, out var list)) + { + list.Add(value); + } + else + { + result.Add(key, new List() {value}); + } + } + + return result; + } + } +} diff --git a/src/ErrorProne.NET.Core/ErrorProne.NET.Core.csproj b/src/ErrorProne.NET.Core/ErrorProne.NET.Core.csproj index 6e4d00c..cf9f903 100644 --- a/src/ErrorProne.NET.Core/ErrorProne.NET.Core.csproj +++ b/src/ErrorProne.NET.Core/ErrorProne.NET.Core.csproj @@ -1,14 +1,25 @@  - netstandard1.3 + netstandard2.0 + + + + preview - - - + + + - + + + Microsoft.CodeAnalysis;Microsoft.CodeAnalysis.CSharp + + + + + diff --git a/src/ErrorProne.NET.Core/ForEachAnalysisHelper.cs b/src/ErrorProne.NET.Core/ForEachAnalysisHelper.cs new file mode 100644 index 0000000..464e1ce --- /dev/null +++ b/src/ErrorProne.NET.Core/ForEachAnalysisHelper.cs @@ -0,0 +1,34 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Operations; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Text; + +namespace ErrorProne.NET.Core +{ + // TODO: rename to unsafe? + public static class ForEachAnalysisHelper + { + public static IMethodSymbol GetEnumeratorMethod(this IForEachLoopOperation foreachLoop) + { + var loop = (BaseForEachLoopOperation)foreachLoop; + + return loop.Info.GetEnumeratorMethod; + } + + public static ITypeSymbol GetElementType(this IForEachLoopOperation foreachLoop) + { + var loop = (BaseForEachLoopOperation)foreachLoop; + + return loop.Info.ElementType; + } + + public static Conversion? GetConversionInfo(this IForEachLoopOperation foreachLoop) + { + var loop = (BaseForEachLoopOperation)foreachLoop; + return loop.Info.ElementConversion as Conversion?; + } + } +} diff --git a/src/ErrorProne.NET.Core/LocalScopeProvider.cs b/src/ErrorProne.NET.Core/LocalScopeProvider.cs new file mode 100644 index 0000000..f605f7e --- /dev/null +++ b/src/ErrorProne.NET.Core/LocalScopeProvider.cs @@ -0,0 +1,72 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using System.Diagnostics; +using System.Linq; +using Microsoft.CodeAnalysis.CSharp.Symbols; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +#nullable enable + +namespace ErrorProne.NET.Core +{ + public static class LocalScopeProvider + { + public static SyntaxNode? GetScopeForDisplayClass(this ISymbol symbol) + { + // This method uses internal Roslyn API + + if (symbol is SourceLocalSymbol localSymbol) + { + // For loop is special: a local declared in for-loop is scoped to the loop based on + // the language rules, but at "runtime" it actually kind of defined in the enclosing scope. + // It means that if an anonymous method captures a for variable, a display class is instantiated + // at the beginning of a parent scope, but not inside the for loop. + + if (localSymbol.ScopeBinder is ForLoopBinder fb) + { + return fb.Next.ScopeDesignator; + } + + return localSymbol.ScopeDesignatorOpt; + } + + if (symbol is IParameterSymbol) + { + var declaredIn = symbol.ContainingSymbol; + var (blockBody, arrowBody) = declaredIn.GetBodies(); + return (SyntaxNode)blockBody ?? arrowBody; + } + + return null; + } + + internal static (BlockSyntax blockBody, ArrowExpressionClauseSyntax arrowBody) GetBodies(this ISymbol methodSymbol) + { + var syntax = methodSymbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(); + + { + switch (syntax) + { + case BaseMethodDeclarationSyntax method: + return (method.Body, method.ExpressionBody); + + case AccessorDeclarationSyntax accessor: + return (accessor.Body, accessor.ExpressionBody); + + case ArrowExpressionClauseSyntax arrowExpression: + Debug.Assert(arrowExpression.Parent.Kind() == SyntaxKind.PropertyDeclaration || + arrowExpression.Parent.Kind() == SyntaxKind.IndexerDeclaration || + methodSymbol is SynthesizedClosureMethod); + return (null, arrowExpression); + + case BlockSyntax block: + Debug.Assert(methodSymbol is SynthesizedClosureMethod); + return (block, null); + + default: + return (null, null); + } + } + } + } +} diff --git a/src/ErrorProne.NET.Core/SymbolExtensions.cs b/src/ErrorProne.NET.Core/SymbolExtensions.cs index 2158f82..aa3616e 100644 --- a/src/ErrorProne.NET.Core/SymbolExtensions.cs +++ b/src/ErrorProne.NET.Core/SymbolExtensions.cs @@ -6,6 +6,8 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +#nullable enable + namespace ErrorProne.NET.Core { /// @@ -42,7 +44,42 @@ public static IEnumerable GetAllUsedSymbols(Compilation compilation, Sy } } - public static bool TryGetMethodSyntax(this IMethodSymbol method, out MethodDeclarationSyntax result) + public static ITypeSymbol? GetSymbolType(this ISymbol symbol) + { + if (symbol is ILocalSymbol localSymbol) + { + return localSymbol.Type; + } + + if (symbol is IFieldSymbol fieldSymbol) + { + return fieldSymbol.Type; + } + + if (symbol is IPropertySymbol propertySymbol) + { + return propertySymbol.Type; + } + + if (symbol is IParameterSymbol parameterSymbol) + { + return parameterSymbol.Type; + } + + if (symbol is IAliasSymbol aliasSymbol) + { + return aliasSymbol.Target as ITypeSymbol; + } + + if (symbol is IMethodSymbol ms) + { + return ms.ReturnType; + } + + return symbol as ITypeSymbol; + } + + public static bool TryGetMethodSyntax(this IMethodSymbol method, out MethodDeclarationSyntax? result) { result = method.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() as MethodDeclarationSyntax; return result != null; diff --git a/src/ErrorProne.NET.Core/ValueTupleHelper.cs b/src/ErrorProne.NET.Core/ValueTupleHelper.cs new file mode 100644 index 0000000..fdc925a --- /dev/null +++ b/src/ErrorProne.NET.Core/ValueTupleHelper.cs @@ -0,0 +1,16 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Symbols; +using System.Linq; + +namespace ErrorProne.NET.Core +{ + public static class ValueTupleHelper + { + public static ITypeSymbol[] GetTupleTypes(this ITypeSymbol tupleType) + { + var tuple = (TupleTypeSymbol)tupleType; + + return tuple.TupleElementTypesWithAnnotations.Select(t => (ITypeSymbol)t.Type).ToArray(); + } + } +} diff --git a/src/ErrorProne.NET.CoreAnalyzers.CodeFixes/ErrorProne.NET.CoreAnalyzers.CodeFixes.csproj b/src/ErrorProne.NET.CoreAnalyzers.CodeFixes/ErrorProne.NET.CoreAnalyzers.CodeFixes.csproj index 553dc2a..0012a65 100644 --- a/src/ErrorProne.NET.CoreAnalyzers.CodeFixes/ErrorProne.NET.CoreAnalyzers.CodeFixes.csproj +++ b/src/ErrorProne.NET.CoreAnalyzers.CodeFixes/ErrorProne.NET.CoreAnalyzers.CodeFixes.csproj @@ -1,7 +1,6 @@  - netstandard1.3 ErrorProne.Net.CoreAnalyzers.CodeFixes ErrorProne.NET false @@ -11,7 +10,7 @@ - netstandard1.3 + netstandard2.0 @@ -53,6 +52,8 @@ + + diff --git a/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/AllocationTestHelper.cs b/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/AllocationTestHelper.cs new file mode 100644 index 0000000..2ea1b28 --- /dev/null +++ b/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/AllocationTestHelper.cs @@ -0,0 +1,75 @@ +using System; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using RoslynNUnitTestRunner; + +namespace ErrorProne.NET.CoreAnalyzers.Tests.Allocations +{ + + static class AllocationTestHelper + { + public static void VerifyCode(string code, bool injectAssemblyLevelConfigurationAttribute = true) where TAnalyzer : DiagnosticAnalyzer, new() + { + // enable all the allocation analyzers by adding an assembly level attribute + VerifyCodeAsync(code, injectAssemblyLevelConfigurationAttribute).GetAwaiter().GetResult(); + } + + public static Task VerifyCodeAsync(string code, bool injectAssemblyLevelConfigurationAttribute = true) where TAnalyzer : DiagnosticAnalyzer, new() + { + // enable all the allocation analyzers by adding an assembly level attribute + return VerifyCodeImpl(code, injectAssemblyLevelConfigurationAttribute); + } + + private static Task VerifyCodeImpl(string code, bool injectAssemblyLevelConfigurationAttribute = false) where TAnalyzer : DiagnosticAnalyzer, new() + { + var test = new CSharpCodeFixVerifier.Test + { + TestState = + { + Sources = + { + code, + }, + }, + }.WithoutGeneratedCodeVerification().WithHiddenAllocationsAttributeDeclaration(); + + if (injectAssemblyLevelConfigurationAttribute) + { + test = test.WithAssemblyLevelHiddenAllocationsAttribute(); + } + + return test.RunAsync(); + } + } + + struct Struct + { + public void Method() + { + } + + public static void StaticMethod() + { + } + } + + struct StructWithOverrides + { + public override string ToString() => string.Empty; + public override int GetHashCode() => 42; + public override bool Equals(object other) => true; + } + + struct ComparableStruct : IComparable + { + public int CompareTo(object obj) => 0; + } + + [Flags] + enum E + { + V1 = 1, + V2 = 1 << 1 + } +} diff --git a/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/ClosureAllocationAnalyzerAnonymousDelegateTests.cs b/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/ClosureAllocationAnalyzerAnonymousDelegateTests.cs new file mode 100644 index 0000000..8f0c5e9 --- /dev/null +++ b/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/ClosureAllocationAnalyzerAnonymousDelegateTests.cs @@ -0,0 +1,93 @@ +using NUnit.Framework; +using RoslynNUnitTestRunner; +using System.Threading.Tasks; +using VerifyCS = RoslynNUnitTestRunner.CSharpCodeFixVerifier< + ErrorProne.NET.CoreAnalyzers.Allocations.ClosureAllocationAnalyzer, + Microsoft.CodeAnalysis.Testing.EmptyCodeFixProvider>; + +namespace ErrorProne.NET.CoreAnalyzers.Tests +{ + [TestFixture] + public class ClosureAllocationAnalyzerAnonymousDelegateTests + { + [Test] + public async Task Closure_Is_Allocated_In_Method() + { + + await ValidateCodeAsync(@" +class A { + public void CapturedLocal(int v) + [|{|] + // Closure allocation here + int n = 42; + // Delegate allocation here. + System.Func a = delegate(int l) {return n + v + l;}; + a(42); + } +}"); + } + + [Test] + public async Task Closure_Is_Allocated_In_Constructor() + { + await ValidateCodeAsync(@" +class A { + public A(int v) + [|{|] + // Closure allocation here + int n = 42; + // Delegate allocation here. + System.Func a = delegate(int l) {return n + v + l;}; + a(42); + } +}"); + } + + [Test] + public async Task Closure_Is_Allocated_In_Method_With_Expression_Body() + { + await ValidateCodeAsync(@" +class A { + private int x = 42; + public System.Func ProducesFunc(int x) [|=>|] delegate() {return x;}; +}"); + } + + [Test] + public async Task Closures_Are_Allocated_In_3_Scopes() + { + await ValidateCodeAsync(@" +class A { + public void F(int n) + [|{|] + // Closure allocation + int scope1 = 1; + [|{|] + // Closure allocation + int scope2 = 2; + + if (n > 10) + [|{|] + // Closure allocation! + int scope3 = 3; + // Delegate allocation + System.Func f2 = delegate() {return scope1 + scope2 + scope3;}; + f2(); + } + } + } +}"); + } + + private Task ValidateCodeAsync(string code) + { + return new VerifyCS.Test + { + TestState = + { + Sources = { code }, + }, + }.WithoutGeneratedCodeVerification().WithHiddenAllocationsAttributeDeclaration().WithAssemblyLevelHiddenAllocationsAttribute().RunAsync(); + } + } +} \ No newline at end of file diff --git a/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/ClosureAllocationAnalyzerLambdaTests.cs b/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/ClosureAllocationAnalyzerLambdaTests.cs new file mode 100644 index 0000000..895b242 --- /dev/null +++ b/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/ClosureAllocationAnalyzerLambdaTests.cs @@ -0,0 +1,266 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using System.Threading.Tasks; +using ErrorProne.NET.CoreAnalyzers.Allocations; +using ErrorProne.NET.CoreAnalyzers.Tests.Allocations; + +namespace ErrorProne.NET.CoreAnalyzers.Tests +{ + [TestFixture] + public class ClosureAllocationAnalyzerTests + { + static Task ValidateCodeAsync(string code) => AllocationTestHelper.VerifyCodeAsync(code); + + public void ClosureAllocations(int arg) + { + // Delegate allocation for 'n' + int n = 42; + int k = 1; + + //// Delegate allocation for a. + Func a = () => n + k + 1; + + // Delegate allocation for 'this' + Action a2 = () => ClosureAllocations(42); + } + + private void Foo(int[] numbers) + { + int x = 42; + List actions = new List(); + foreach (var n in numbers) + { + actions.Add(() => Console.WriteLine(x + n)); + } + } + + public void AllocationLocalFunctionForLocalFunction2() + { + // Closure allocation + int n = 42; + int local() => n; + local(); + + //Func f = local; + //f(); + } + + [Test] + public async Task Closure_Is_Allocated_In_Method() + { + await ValidateCodeAsync(@" +class A { + public void CapturedLocal(int v) + [|{|] + // Closure allocation here + int n = 42; + // Delegate allocation here. + System.Func a = l => n + v + l; + a(42); + } +}"); + } + + [Test] + public async Task Closure_Is_Allocated_In_Constructor() + { + await ValidateCodeAsync(@" +class A { + public A(int v) + [|{|] + // Closure allocation here + int n = 42; + // Delegate allocation here. + System.Func a = l => n + v + l; + a(42); + } +}"); + } + + [Test] + public async Task Closure_Is_Allocated_In_Method_With_Expression_Body() + { + await ValidateCodeAsync(@" +class A { + private int x = 42; + public System.Func ProducesFunc(int x) [|=>|] () => x; +}"); + } + + [Test] + public async Task Closure_Is_Allocated_In_Properties() + { + await ValidateCodeAsync(@" +class A { + private int x = 42; + // No closure allocations, because lambda captures only 'this'. + public bool P1 => ((System.Func)(() => x > 42))(); + + public int P2 + { + // No closure allocations, because lambda captures only 'this'. + get => ((System.Func)(() => x > 42 ? 1 : 2))(); + set [|=>|] ((System.Func)(() => value > 42))(); + } + + public bool P3 + { + get [|{|] int z = 42;return ((System.Func)(() => z > 42))(); } + } +}"); + } + + [Test] + public async Task Closure_Is_Allocated_In_If_Block() + { + await ValidateCodeAsync(@" +class A { + public void M(int y) + { + if (y > 0) + [|{|] + // Closure allocation + int x = 42; + System.Func f = () => x; + f(); + } + } +}"); + } + + [Test] + public async Task Closure_Is_Not_Allocated_When_Instance_Or_Static_Fields_Were_Used() + { + await ValidateCodeAsync(@" +class A { + private static int s = 42; + private int i = 42; + public void M(int y) + { + // Delegate allocation, but no closure allocations. + System.Func f = () => s + i; + f(); + } +}"); + } + + [Test] + public async Task Closure_Is_Allocated_In_The_Beginning_Event_If_Used_In_Nested_If_Block() + { + await ValidateCodeAsync(@" +class A { + public void M(int y) + [|{|] + // Closure allocation + if (y > 0) + { + // Closure allocation + System.Func f = () => y; + f(); + } + } +}"); + } + + [Test] + public async Task Closures_Are_Allocated_In_Two_Scopes() + { + await ValidateCodeAsync(@" +class A { + public void F(int a) + [|{|] + // The first closure is allocated here + if (a > 10) + [|{|] + // And the second closure is allocated here + int k = 42; + System.Func f2 = () => a + k; + f2(); + } + } +}"); + } + + [Test] + public async Task Closures_Are_Allocated_In_3_Scopes() + { + await ValidateCodeAsync(@" +class A { + public void F(int n) + [|{|] + // Closure allocation + int scope1 = 1; + [|{|] + // Closure allocation + int scope2 = 2; + + if (n > 10) + [|{|] + // Closure allocation! + int scope3 = 3; + // Delegate allocation + System.Func f2 = () => scope1 + scope2 + scope3; + f2(); + } + } + } +}"); + } + + [Test] + public async Task Closure_Is_Allocated_In_ForEach_Loop_No_Block() + { + await ValidateCodeAsync(@" +using System; +using System.Collections.Generic; +class A { + private void Foo(int[] numbers) + [|{|] + int x = 42; + List actions = new List(); + [|foreach|](var n in numbers) + actions.Add(() => Console.WriteLine(x + n)); + } +}"); + } + + [Test] + public async Task Closure_Is_Allocated_In_ForEach_Loop_Block() + { + await ValidateCodeAsync(@" +using System; +using System.Collections.Generic; +class A { + private void Foo(int[] numbers) + [|{|] + int x = 42; + List actions = new List(); + foreach(var n in numbers) + [|{|] + actions.Add(() => Console.WriteLine(x + n)); + } + } +}"); + } + + [Test] + public async Task Closure_Is_Allocated_In_For_Loop() + { + await ValidateCodeAsync(@" +using System; +using System.Collections.Generic; +class A { + private void Foo() + [|{|] + List actions = new List(); + for(int n = 12; n < 5; n+=2) + [|{|] + int k = n; + actions.Add(() => Console.WriteLine(n + k)); + } + } +}"); + } + } +} \ No newline at end of file diff --git a/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/ClosureAllocationAnalyzerLocalFunctionTests.cs b/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/ClosureAllocationAnalyzerLocalFunctionTests.cs new file mode 100644 index 0000000..4c413eb --- /dev/null +++ b/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/ClosureAllocationAnalyzerLocalFunctionTests.cs @@ -0,0 +1,96 @@ +using NUnit.Framework; +using System.Threading.Tasks; +using ErrorProne.NET.CoreAnalyzers.Allocations; +using ErrorProne.NET.CoreAnalyzers.Tests.Allocations; + +namespace ErrorProne.NET.CoreAnalyzers.Tests +{ + [TestFixture] + public class ClosureAllocationAnalyzerLocalFunctionTests + { + static Task ValidateCodeAsync(string code) => AllocationTestHelper.VerifyCodeAsync(code); + + [Test] + public async Task Closure_Is_Allocated_When_Local_Function_Is_Converted_To_Delegate() + { + await ValidateCodeAsync(@" +using System; +using System.Collections.Generic; +class A { + public void M(int arg) + [|{|] + // This will cause the closure to be a class, not a struct + if (arg > 2) + { + Func fn = local; + } + + int v = local(); + int local() => arg; + } +}"); + } + + [Test] + public async Task Closure_Is_Allocated_When_Local_Function_Is_Converted_To_Delegate2() + { + await ValidateCodeAsync(@" +using System; +using System.Collections.Generic; +class A { + private void Foo(Func f) {} + public void M(int arg) + [|{|] + // This will cause the closure to be a class, not a struct + if (arg > 2) + { + Foo(local); + } + + int v = local(); + int local() => arg; + } +}"); + } + + + [Test] + public async Task No_Closure_Is_Allocated_When_Local_Function_Captures_This_Only() + { + await ValidateCodeAsync(@" +using System; +using System.Collections.Generic; +class A { + private int x = 42; + private void Foo(Func f) {} + public void M(int arg) + { + // No closure allocations, because the local function captures 'this' but nothing else. + // In this case the delegate is allocated at Foo(local), but no closure is created. + if (arg > 2) + { + Foo(local); + } + + int v = local(); + int local() => x; + } +}"); + } + + [Test] + public async Task No_Closure_Is_Allocated_When_Local_Function_Is_Not_Converted_To_Delegate() + { + await ValidateCodeAsync(@" +using System; +using System.Collections.Generic; +class A { + public void M(int arg) + { + int v = local(); + int local() => arg; + } +}"); + } + } +} \ No newline at end of file diff --git a/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/DelegateAllocationAnalyzerTests.cs b/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/DelegateAllocationAnalyzerTests.cs new file mode 100644 index 0000000..f28ee01 --- /dev/null +++ b/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/DelegateAllocationAnalyzerTests.cs @@ -0,0 +1,59 @@ +using NUnit.Framework; +using System.Threading.Tasks; +using ErrorProne.NET.CoreAnalyzers.Allocations; +using ErrorProne.NET.CoreAnalyzers.Tests.Allocations; + +namespace ErrorProne.NET.CoreAnalyzers.Tests +{ + [TestFixture] + public class DelegateAllocationAnalyzerTests + { + static Task ValidateCodeAsync(string code) => + AllocationTestHelper.VerifyCodeAsync(code); + + [Test] + public async Task Delegate_Allocation_For_Simple_Lambda() + { + await ValidateCodeAsync(@" +class A { + public void Foo() + { + System.Action a = () [|=>|] Foo(); + } +}"); + } + + [Test] + public async Task Delegate_Allocation_For_Method_Group_Conversion() + { + await ValidateCodeAsync(@" +class A { + public void Foo() + { + System.Action a = [|Foo|]; + System.Action a2 = [|localFunction|]; + Bar([|Foo|]); + void localFunction() {} + } + + private void Bar(System.Action a) {} +}"); + } + + [Test] + public async Task No_Delegate_Allocation_For_Non_Capturing_Lambda() + { + await ValidateCodeAsync(@" +class A { + public void Foo() + { + System.Action a = () => Bar(staticValue, GetValue()); + } + + private static void Bar(int x, int y) {} + private static int staticValue = 42; + private static int GetValue() => 42; +}"); + } + } +} \ No newline at end of file diff --git a/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/ImplicitBoxingAnalyzerTests.cs b/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/ImplicitBoxingAnalyzerTests.cs new file mode 100644 index 0000000..ef8792f --- /dev/null +++ b/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/ImplicitBoxingAnalyzerTests.cs @@ -0,0 +1,190 @@ +using ErrorProne.NET.CoreAnalyzers.Allocations; +using JetBrains.dotMemoryUnit; +using JetBrains.dotMemoryUnit.Kernel; +using NUnit.Framework; + +namespace ErrorProne.NET.CoreAnalyzers.Tests.Allocations +{ + [TestFixture] + public class ImplicitBoxingAnalyzerTests + { + static void VerifyCode(string code) => AllocationTestHelper.VerifyCode(code); + + [Test] + public void Calling_Overrides_Does_Not_Cause_Boxing() + { + VerifyCode(@" +struct S { + public override string ToString() => string.Empty; + public override int GetHashCode() => 42; + public override bool Equals(object other) => true; +} + +class A { + static S GetS() => default; + static S MyS => default; + + static void Main() { + S s = default; + + var hc2 = GetS().GetHashCode(); + var e2 = s.Equals(default); + var str = MyS.ToString(); + var t = GetS().GetType(); + } +}"); + + if (!dotMemoryApi.IsEnabled) return; + + StructWithOverrides s = default; + + var checkpoint = dotMemory.Check(); + + var hc2 = s.GetHashCode(); + var e2 = s.Equals(default); + var str2 = s.ToString(); + var t = s.GetType(); + + dotMemory.Check(check => + Assert.AreEqual( + 0, + check.GetDifference(checkpoint).GetNewObjects(where => where.Type.Is()).ObjectsCount)); + } + + [Test] + public void Extension_Method_On_Struct_Should_Cause_No_Warnings() + { + VerifyCode(@" +interface I {} +struct S : I {} +static class E { + public static void Foo(this S i) {} +} +class A { + void M() { + S s = default; + s.Foo(); + } +}"); + } + + [Test] + public void Calling_GetHashCode_On_Generic_Should_Not_Cause_Warning() + { + VerifyCode(@" +class A { + // This should not cause any warnings. + static int GenericGetHashCode(T t) where T: struct => t.GetHashCode(); + + static string GenericToString(T t) where T: struct => t.ToString(); +} + +class GA where T : struct +{ + static int GenericGetHashCode() + { + T t = default; + return t.GetHashCode(); + } +} + +"); + } + + [Test] + public void Interface_Method_Call_Causes_No_Boxing_When_Called_Via_Generics_With_Constraints() + { + VerifyCode(@" +interface IFoo {void Foo();} +static class A { + static void CallFoo(T foo) where T : struct, IFoo => foo.Foo(); +}"); + } + + [Test] + public void Calling_Non_Override_On_Struct_Causes_Boxing() + { + VerifyCode(@" +namespace FooBar { +struct S { } +class A { + static S GetS() => default; + static S MyS => default; + + // This should not cause any warnings. + static int GetHashCodeGeneric(T t) where T: struct => t.GetHashCode(); + + static void Main() { + S s = default; + + string str2 = GetS().[|ToString|](); + var hc3 = [|s.GetHashCode|](); + var e3 = MyS.[|Equals|](default); + var type2 = GetS().GetType(); // Should not warn + +}} +}"); + + if (!dotMemoryApi.IsEnabled) return; + + Struct s = default; + + var checkpoint = dotMemory.Check(); + + var hc = s.GetHashCode(); + + var checkpoint3 = dotMemory.Check(check => + Assert.AreEqual( + 1, + check.GetDifference(checkpoint).GetNewObjects(where => @where.Type.Is()).ObjectsCount)); + + var e = s.Equals(default); + + var checkpoint4 = dotMemory.Check(check => + Assert.AreEqual( + 1, + check.GetDifference(checkpoint3).GetNewObjects(where => where.Type.Is()).ObjectsCount)); + + var str = s.ToString(); + + var checkpoint5 = dotMemory.Check(check => + Assert.AreEqual( + 1, + check.GetDifference(checkpoint4).GetNewObjects(where => where.Type.Is()).ObjectsCount)); + + var t = s.GetType(); + + dotMemory.Check(check => + Assert.AreEqual( + 0, + check.GetDifference(checkpoint5).GetNewObjects(where => where.Type.Is()).ObjectsCount)); + } + + [Test] + public void HasFlag_Causes_Implicit_Boxing_Allocation() + { + VerifyCode(@" +[System.Flags] +enum E { + V1 = 1, + V2 = 1 << 1, +} + +class A { + static bool TestHasFlags(E e) => e.[|HasFlag|](E.V2); +}"); + + if (!dotMemoryApi.IsEnabled) return; + + E e = default; + var checkpoint = dotMemory.Check(); + + var b = e.HasFlag(E.V1); + + dotMemory.Check(check => + Assert.AreEqual( + 2, // e and E.V1 are both boxed + check.GetDifference(checkpoint).GetNewObjects(where => where.Type.Is()).ObjectsCount)); + } + } +} \ No newline at end of file diff --git a/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/ImplicitCastBoxingAnalyzerTests.cs b/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/ImplicitCastBoxingAnalyzerTests.cs new file mode 100644 index 0000000..6db972a --- /dev/null +++ b/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/ImplicitCastBoxingAnalyzerTests.cs @@ -0,0 +1,371 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using ErrorProne.NET.CoreAnalyzers.Allocations; +using JetBrains.dotMemoryUnit; +using JetBrains.dotMemoryUnit.Kernel; + +namespace ErrorProne.NET.CoreAnalyzers.Tests.Allocations +{ + [TestFixture] + public class ImplicitCastBoxingAnalyzerTests + { + static void VerifyCode(string code) => + AllocationTestHelper.VerifyCode(code); + + [Test] + public void ValueTuple_Conversion_Causes_Boxing() => VerifyCode(@" +using System; +struct S {} +static class A { + static (S, S) _tuple = (default, default); + static (object, object) M() => [|_tuple|]; + static (object, object) N() => ([|default(S)|], [|default(S)|]); +}"); + + [Test] + public void ValueTuple_Conversion_Causes_No_Boxing_For_User_Defined_Conversion() => VerifyCode(@" +public class C {} +public struct MyStruct +{ + public static implicit operator C(MyStruct ms) => null; + public static (C, C) Test() => (new MyStruct(), new MyStruct()); + public static (object, object) Test2() => (new MyStruct(), new MyStruct()); +}"); + + [Test] + public void Implicit_Conversion_In_String_Construction_Causes_Boxing() + { + VerifyCode(@" +using System; +using System.Collections.Generic; +using System.Linq; +struct S {} +static class A { + static void Main() + { + int n = 42; + + string s = string.Empty + [|n|]; + + string s2 = $""{[|n|]}""; + } +}"); + if (!dotMemoryApi.IsEnabled) return; + + Struct s = default; + + var checkpoint = dotMemory.Check(); + + string str1 = string.Empty + s; + + var checkpoint2 = dotMemory.Check(check => + Assert.AreEqual( + 1, + check.GetDifference(checkpoint).GetNewObjects(where => where.Type.Is()).ObjectsCount)); + + string str2 = $"{s}"; + + dotMemory.Check(check => + Assert.AreEqual( + 1, + check.GetDifference(checkpoint2).GetNewObjects(where => where.Type.Is()).ObjectsCount)); + } + + [Test] + public void Implicit_Conversion_Causes_Boxing() + { + VerifyCode(@" +using System; +using System.Collections.Generic; +using System.Linq; +struct S {} +static class A { + static void Main() + { + object o = [|42|]; + o = [|52|]; + IConvertible c = [|42|]; + c = [|52|]; + + // argument conversion + foo([|42|]); + void foo(object arg) {} + + // return statement conversion + object bar(int n) => [|n|]; + + object x = string.Empty + [|42|]; + } +}"); + + if (!dotMemoryApi.IsEnabled) return; + + var checkpoint = dotMemory.Check(); + + object o = default(Struct); + + var checkpoint2 = dotMemory.Check(check => + Assert.AreEqual( + 1, + check.GetDifference(checkpoint).GetNewObjects(where => where.Type.Is()).ObjectsCount)); + + o = default(Struct); + + var checkpoint3 = dotMemory.Check(check => + Assert.AreEqual( + 1, + check.GetDifference(checkpoint2).GetNewObjects(where => where.Type.Is()).ObjectsCount)); + + IComparable c = default(ComparableStruct); + + var checkpoint4 = dotMemory.Check(check => + Assert.AreEqual( + 1, + check.GetDifference(checkpoint3).GetNewObjects(where => where.Type.Is()) + .ObjectsCount)); + + // argument conversion + MemoryCheckPoint checkpoint5; + + void MethodTakesObject(object arg) + { + checkpoint5 = dotMemory.Check(check => + Assert.AreEqual( + 1, + check.GetDifference(checkpoint4).GetNewObjects(where => where.Type.Is()).ObjectsCount)); + } + + MethodTakesObject(default(Struct)); + + // return statement conversion + object ReturnAsObject(Struct n) => n; + o = ReturnAsObject(default); + + dotMemory.Check(check => + Assert.AreEqual( + 1, + check.GetDifference(checkpoint5).GetNewObjects(where => where.Type.Is()).ObjectsCount)); + } + + [Test] + public void Implicit_Conversion_In_Params_Causes_Boxing() + { + VerifyCode(@" +static class A { + static void Bar(params object[] p) + { + } + static void Main() + { + Bar([|42|]); + } +}"); + if (!dotMemoryApi.IsEnabled) return; + + var checkpoint = dotMemory.Check(); + + Bar(default(Struct)); + + void Bar(params object[] p) + { + dotMemory.Check(check => + Assert.AreEqual( + 1, + check.GetDifference(checkpoint).GetNewObjects(where => where.Type.Is()).ObjectsCount)); + } + } + + [Test] + public void Implicit_Boxing_For_Delegate_Construction_From_Struct() + { + VerifyCode(@" +using System; +public class C { + public void M() {} +} + +public struct S { + public void M() { + } + + public static void StaticM() {} + + public static void Run() + { + C c = new C(); + S s = new S(); + + // c is not boxed! + Action a = c.M; + a(); + + // s is boxed + a = new Action([|s|].M); + a(); + + // nothing to box + a = new Action(S.StaticM); + a(); + } +}"); + if (!dotMemoryApi.IsEnabled) return; + + Struct s = default; + + var checkpoint = dotMemory.Check(); + + var a = new Action(s.Method); + var a2 = new Action(Struct.StaticMethod); + + dotMemory.Check(check => + Assert.AreEqual( + 1, + check.GetDifference(checkpoint).GetNewObjects(where => where.Type.Is()).ObjectsCount)); + } + + [Test] + public void Yield_Return_Int_Causes_Boxing() + { + VerifyCode(@" +using System; +using System.Collections.Generic; +using System.Linq; +struct S {} +static class A { + static IEnumerable Foo() + { + yield return [|42|]; + } +}"); + if (!dotMemoryApi.IsEnabled) return; + + var checkpoint = dotMemory.Check(); + + foreach (object obj in GetEnumerable()) + { + dotMemory.Check(check => + Assert.AreEqual( + 1, + check.GetDifference(checkpoint).GetNewObjects(where => where.Type.Is()).ObjectsCount)); + } + + IEnumerable GetEnumerable() + { + yield return default(Struct); + } + } + + [Test] + public void Test_Implicit_Boxing_When_Foreach_Upcasts_Int_To_Object() + { + VerifyCode(@" +using System; +using System.Collections.Generic; +using System.Linq; +struct S {} +static class A { + static void Main() + { + foreach(object obj in [|Enumerable.Range(1,10)|]) {} + foreach(object obj in [|new int[0]|]) {} + } +}"); + + if (!dotMemoryApi.IsEnabled) return; + + var checkpoint = dotMemory.Check(); + + foreach (object obj in new Struct[] {default}) + { + dotMemory.Check(check => + Assert.AreEqual( + 1, + check.GetDifference(checkpoint).GetNewObjects(where => where.Type.Is()).ObjectsCount)); + } + } + + [Test] + public void Interface_Method_Call_Causes_Boxing() + { + VerifyCode(@" +interface IFoo {void Foo();} +struct S : IFoo {public void Foo() {}} +static class A { + static void CallFoo(IFoo foo) => foo.Foo(); + static void Main() + { + S s = default; + CallFoo([|s|]); + } +}"); + } + + [Test] + public void Cast_To_ValueType() + { + VerifyCode(@" +struct S { } +class A { + void M(){ + System.ValueType vt = [|default(S)|]; + } +}"); + } + + [Test] + public void Cast_To_Enum() + { + VerifyCode(@" +enum E { A } +class A { + void M() { + System.Enum e = [|E.A|]; + } +}"); + } + + [Test] + public void Extension_Method_On_Interface_Causes_Boxing() + { + VerifyCode(@" +interface I {} +struct S : I {} +static class E { + public static void Foo(this S i) {} + public static void Bar(this I i) {} +} +class A { + void M() { + S s = default; + s.Foo(); + [|s|].Bar(); + } +}"); + } + + [Test] + public void Params_Arguments_Causes_Boxing() => + VerifyCode(@" +struct S {} +class A { + static void N(params object[] parameters){} + void M() { + N([|1|], [|default(S)|], string.Empty); + } +}"); + + [Test] + public void UserDefined_Implicit_Cast_Is_Ignored() => + VerifyCode(@" +struct S { + public static implicit operator string(S s) => null; +} +class A { + void M() { + string str = default(S); + } +} +"); + } +} \ No newline at end of file diff --git a/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/ImplicitEnumeratorAllocationAnalyzerTests.cs b/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/ImplicitEnumeratorAllocationAnalyzerTests.cs new file mode 100644 index 0000000..a33d5ac --- /dev/null +++ b/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/ImplicitEnumeratorAllocationAnalyzerTests.cs @@ -0,0 +1,195 @@ +using ErrorProne.NET.CoreAnalyzers.Allocations; +using NUnit.Framework; +using RoslynNUnitTestRunner; +using System.Threading.Tasks; +using VerifyCS = RoslynNUnitTestRunner.CSharpCodeFixVerifier< + ErrorProne.NET.CoreAnalyzers.Allocations.ImplicitEnumeratorAllocationAnalyzer, + Microsoft.CodeAnalysis.Testing.EmptyCodeFixProvider>; + +namespace ErrorProne.NET.CoreAnalyzers.Tests.Allocations +{ + [TestFixture] + public class ImplicitEnumeratorAllocationAnalyzerTests + { + static void VerifyCode(string code) => AllocationTestHelper.VerifyCode(code); + + [Test] + public void Foreach_On_Iterator_BLock_Causes_Allocation() + { + VerifyCode(@" +using System.Collections.Generic; + +class A { + static IEnumerable Generate() + { + yield break; + } + + static void M() + { + foreach(var e in [|Generate()|]) + { + System.Console.WriteLine(e); + } + } +}"); + } + + [Test] + public void Foreach_On_Interface_Causes_Boxing() + { + VerifyCode(@" +using System.Collections.Generic; + +class A { + static void M(IList list) + { + foreach(var e in [|list|]) + { + System.Console.WriteLine(e); + } + } +}"); + } + + [Test] + public void Foreach_On_IListExpression_Causes_Boxing() + { + VerifyCode(@" +using System.Collections.Generic; + +class A { + + static IList GetList() => null; + + static void M() + { + foreach(var e in [|GetList()|]) + { + System.Console.WriteLine(e); + } + } +}"); + } + + [Test] + public void Foreach_On_Casted_Interface_Causes_Boxing() + { + VerifyCode(@" +using System.Collections.Generic; + +class A { + static void M(List list) + { + foreach(var e in list) + { + System.Console.WriteLine(e); + } + + foreach(var e in [|(IList)list|]) + { + System.Console.WriteLine(e); + } + } +}"); + } + + + + [Test] + public void Foreach_On_CustomTypeWithStructEnumerator_DoesNotCause_Boxing() + { + VerifyCode(@" +using System.Collections.Generic; + +public struct StructEnumerator { + public object Current { get; } + + public bool MoveNext (){ + return false; + } +} + +public class TheEnumerator{ + public StructEnumerator GetEnumerator(){ + return new StructEnumerator(); + } +} + +class A { + static void M(TheEnumerator list) + { + foreach(var e in list) + { + System.Console.WriteLine(e); + } + } +}"); + } + + [Test] + public void Foreach_On_CustomTypeWithClassEnumerator_Causes_Boxing() + { + VerifyCode(@" +using System.Collections.Generic; + +public class ClassEnumerator { + public object Current { get; } + + public bool MoveNext (){ + return false; + } +} + +public class TheEnumerator{ + public ClassEnumerator GetEnumerator(){ + return new ClassEnumerator(); + } +} + +class A { + static void M(TheEnumerator list) + { + foreach(var e in [|list|]) + { + System.Console.WriteLine(e); + } + } +}"); + } + + [Test] + public void Foreach_On_String_Causes_No_Boxing() + { + VerifyCode(@" +using System.Collections.Generic; + +class A { + static void M(string list) + { + foreach(var e in list) + { + System.Console.WriteLine(e); + } + } +}"); + } + + [Test] + public void Foreach_On_StringArray_Causes_No_Boxing() + { + VerifyCode(@" +using System.Collections.Generic; + +class A { + static void M(string[] list) + { + foreach(var e in list) + { + System.Console.WriteLine(e); + } + } +}"); + } + } +} \ No newline at end of file diff --git a/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/LinqKnownMethodsAnalyzerTests.cs b/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/LinqKnownMethodsAnalyzerTests.cs new file mode 100644 index 0000000..254c3ed --- /dev/null +++ b/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/LinqKnownMethodsAnalyzerTests.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using ErrorProne.NET.CoreAnalyzers.Allocations; +using JetBrains.dotMemoryUnit; +using JetBrains.dotMemoryUnit.Kernel; + +namespace ErrorProne.NET.CoreAnalyzers.Tests.Allocations +{ + [TestFixture] + public class LinqKnownMethodsAnalyzerTests + { + static void VerifyCode(string code) => AllocationTestHelper.VerifyCode(code); + + [Test] + public void Calling_Linq_Causes_Boxing() => VerifyCode(@" +using System.Linq; +struct S { } +class A { + static void Main() { + S[] arr = new S[1]; + S s = arr.[|First|](); + } +}"); + + [Test] + public void Calling_Linq_Count_On_Collection_Does_Not_Cause_Boxing() => VerifyCode(@" +using System.Linq; +struct S { } +class A { + static void Main() { + S[] arr = new S[1]; + int c = arr.Count(); + } +}"); + } +} \ No newline at end of file diff --git a/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/NoHiddenAllocationConfigurationTests.cs b/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/NoHiddenAllocationConfigurationTests.cs new file mode 100644 index 0000000..865c59f --- /dev/null +++ b/src/ErrorProne.NET.CoreAnalyzers.Tests/Allocations/NoHiddenAllocationConfigurationTests.cs @@ -0,0 +1,399 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using ErrorProne.NET.AsyncAnalyzers; +using ErrorProne.NET.CoreAnalyzers.Allocations; +using JetBrains.dotMemoryUnit; +using JetBrains.dotMemoryUnit.Kernel; +using NUnit.Framework; + +namespace ErrorProne.NET.CoreAnalyzers.Tests.Allocations +{ + public static class StringExtensions + { + public static string ReplaceAttribute(this string codeSnippet, string actualAttribute) + { + return codeSnippet.Replace("[NoHiddenAllocations]", actualAttribute); + } + } + + [TestFixture] + public class NoHiddenAllocationConfigurationTests + { + private static void VerifyAllocatingCode(string code) + { + AllocationTestHelper.VerifyCode(code, injectAssemblyLevelConfigurationAttribute: false); + } + + private static object[] NoHiddenAllocationAttributeCombinations = + { + new object[] {"[NoHiddenAllocations]"}, + new object[] {"[NoHiddenAllocations(Recursive = true)]"}, + new object[] {"[NoHiddenAllocations(Recursive = false)]"}, + new object[] {"[NoHiddenAllocations(Recursive = false || true)]"} + }; + + [TestCaseSource(nameof(NoHiddenAllocationAttributeCombinations))] + public void Functions(string noHiddenAllocationAttribute) + { + VerifyAllocatingCode(@" +[NoHiddenAllocations] +class A { + static object F() => [|1|]; + static object G() { + return [|1|]; + } +}".ReplaceAttribute(noHiddenAllocationAttribute)); + } + + [TestCaseSource(nameof(NoHiddenAllocationAttributeCombinations))] + public void Local_Function(string noHiddenAllocationAttribute) + { + VerifyAllocatingCode(@" +class A { + [NoHiddenAllocations] + static object F() { + return $""{local()} {curlyLocal()}""; + + object local() => [|1|]; + + object curlyLocal() + { + return [|1|]; + } + } +}".ReplaceAttribute(noHiddenAllocationAttribute)); + } + + [TestCaseSource(nameof(NoHiddenAllocationAttributeCombinations))] + public void Properties(string noHiddenAllocationAttribute) + { + VerifyAllocatingCode(@" +class A { + + [NoHiddenAllocations] + public object B => [|1|]; + + [NoHiddenAllocations] + public object C { + get => [|1|]; + } + + [NoHiddenAllocations] + public object D { + get { return [|1|];} + } + + public object E { + [NoHiddenAllocations] + get => [|1|]; + + [NoHiddenAllocations] + set { + object o = [|1|]; + } + } + + [NoHiddenAllocations] + public object F { + get { return [|1|]; } + } + + [NoHiddenAllocations] + public object G { + get { + return local(); + + object local() => [|1|]; + } + } +}".ReplaceAttribute(noHiddenAllocationAttribute)); + } + + [TestCaseSource(nameof(NoHiddenAllocationAttributeCombinations))] + public void Partial_Class(string noHiddenAllocationAttribute) + { + VerifyAllocatingCode(@" +[NoHiddenAllocations] +partial class A { } + +partial class A { + + static object F() { + + return [|1|]; + } +}".ReplaceAttribute(noHiddenAllocationAttribute)); + } + + [TestCaseSource(nameof(NoHiddenAllocationAttributeCombinations))] + public async Task Recursive_Application_Is_Enforced(string noHiddenAllocationAttribute) + { + await AllocationTestHelper.VerifyCodeAsync(@" +static class DirectCallsiteClass { + + [NoHiddenAllocations(Recursive = true)] + static void DirectCallsiteMethod() { + IndirectTargetClass.IndirectTargetMethod(); + + DirectTargetClass.DirectTargetMethod(); + [|DirectTargetClass.NonMarkedMethod()|]; + + [|DirectTargetWithoutReceiver()|]; + } + + static void DirectTargetWithoutReceiver(){ + } +} + +[NoHiddenAllocations(Recursive = true)] +static class IndirectCallsiteClass { + static void IndirectCallsiteMethod() { + IndirectTargetClass.IndirectTargetMethod(); + + DirectTargetClass.DirectTargetMethod(); + [|DirectTargetClass.NonMarkedMethod()|]; + + IndirectTargetWithoutReceiver(); + } + + static void IndirectTargetWithoutReceiver(){ + } +} + +[NoHiddenAllocations] +class IndirectTargetClass { + + public static void IndirectTargetMethod() { + } +} + +class DirectTargetClass { + + [NoHiddenAllocations] + public static void DirectTargetMethod() { + } + + public static void NonMarkedMethod() { + } +} +".ReplaceAttribute(noHiddenAllocationAttribute), injectAssemblyLevelConfigurationAttribute: false); + } + + [TestCaseSource(nameof(NoHiddenAllocationAttributeCombinations))] + public async Task Recursive_Application_Callchains(string noHiddenAllocationAttribute) + { + await AllocationTestHelper.VerifyCodeAsync(@" +class A { + [NoHiddenAllocations(Recursive=true)] + static void B(){ + var a = new A(); + [|a.C().D()|].E(); + } + + [NoHiddenAllocations] + A C(){ + return this; + } + + A D(){ + return this; + } + + [NoHiddenAllocations] + A E(){ + return this; + } +} +".ReplaceAttribute(noHiddenAllocationAttribute), injectAssemblyLevelConfigurationAttribute: false); + } + + [Test] + public async Task Recursive_Application_Properties() + { + await AllocationTestHelper.VerifyCodeAsync(@" +class A { + public object B => 1; + + public object C { + get => 1; + } + + public object D { + get { return 1;} + } + + public object E { + get => 1; + + set { + object o = 1; + } + } + + public object F { + get { return 1; } + } + + public object G { + get { + return local(); + + object local() => 1; + } + } + + [NoHiddenAllocations(Recursive=true)] + static void Method(){ + var a = new A(); + + object o = [|a.B|]; + o = [|a.C|]; + o = [|a.D|]; + o = [|a.E|]; + + [|a.E|] = 2; + + o = [|a.F|]; + o = [|a.G|]; + } +} +", injectAssemblyLevelConfigurationAttribute: false); + } + + [Test] + public async Task Recursive_Application_Is_Not_Sensitive_To_Property_Access_Type() + { + await AllocationTestHelper.VerifyCodeAsync(@" +class A { + public object B { + [NoHiddenAllocations] + get => 1; + + set { + object o = 1; + } + } + + public object C { + get => 1; + + [NoHiddenAllocations] + set { + object o = 1; + } + } + + [NoHiddenAllocations] + public object D { + get => 1; + + set { + object o = 1; + } + } + + [NoHiddenAllocations(Recursive=true)] + static void Method(){ + var a = new A(); + + object o = [|a.B|]; + + [|a.C|] = 1; + + o = a.D; + a.D = 2; + } +} +", injectAssemblyLevelConfigurationAttribute: false); + } + + [TestCaseSource(nameof(NoHiddenAllocationAttributeCombinations))] + public async Task Recursive_Application_Is_Sensitive_To_Constructors(string noHiddenAllocationAttribute) + { + await AllocationTestHelper.VerifyCodeAsync(@" +namespace Foo +{ + class A { + [NoHiddenAllocations(Recursive=true)] + static A(){ + StaticC(); + [|StaticD()|]; + } + + [NoHiddenAllocations] + static void StaticC(){ + } + + static void StaticD(){ + } + + [NoHiddenAllocations(Recursive=true)] + void E(){ + object a = new B(); + a = new C(); + a = new D(); + a = [|new E()|]; + a = new E(1); + a = new F(); + a = [|new G()|]; + } + } + + [NoHiddenAllocations] + class B{ + } + + [NoHiddenAllocations] + class C{ + public C(){ + } + } + + class D{ + [NoHiddenAllocations] + public D(){ + } + } + + class E{ + public E(){ + } + + [NoHiddenAllocations] + public E(int a){ + } + } + + class F{ + } + + class G{ + public G(){ + } + } +} +".ReplaceAttribute(noHiddenAllocationAttribute), injectAssemblyLevelConfigurationAttribute: false); + } + + [Test] + public async Task Recursive_Application_Is_Insensitive_To_Library_Code() + { + await AllocationTestHelper.VerifyCodeAsync(@" +using System.Collections.Generic; + +namespace Foo +{ + class A { + [NoHiddenAllocations(Recursive=true)] + void M(){ + var list = new List(); + list.Add(""test""); + + var count = list.Count; + } + } +} +", injectAssemblyLevelConfigurationAttribute: false); + } + } +} diff --git a/src/ErrorProne.NET.CoreAnalyzers.Tests/ErrorProne.NET.CoreAnalyzers.Tests.csproj b/src/ErrorProne.NET.CoreAnalyzers.Tests/ErrorProne.NET.CoreAnalyzers.Tests.csproj index 73f65de..50d9c1f 100644 --- a/src/ErrorProne.NET.CoreAnalyzers.Tests/ErrorProne.NET.CoreAnalyzers.Tests.csproj +++ b/src/ErrorProne.NET.CoreAnalyzers.Tests/ErrorProne.NET.CoreAnalyzers.Tests.csproj @@ -5,8 +5,9 @@ - - + + + diff --git a/src/ErrorProne.NET.CoreAnalyzers/Allocations/ClosureAllocationAnalyzer.cs b/src/ErrorProne.NET.CoreAnalyzers/Allocations/ClosureAllocationAnalyzer.cs new file mode 100644 index 0000000..c451780 --- /dev/null +++ b/src/ErrorProne.NET.CoreAnalyzers/Allocations/ClosureAllocationAnalyzer.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.ContractsLight; +using System.Linq; +using ErrorProne.NET.AsyncAnalyzers; +using ErrorProne.NET.Core; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +#nullable enable + +namespace ErrorProne.NET.CoreAnalyzers.Allocations +{ + internal static class ClosureAnalysisExtensions + { + /// + /// Returns true if the only captured variable is 'this' "pointer". + /// + public static bool CapturesOnlyThis(this DataFlowAnalysis dataflow) + { + return dataflow.Captured.Length == 1 && dataflow.Captured[0] is IParameterSymbol ps && ps.IsThis; + } + } + + /// + /// Analyzer that warns when the result of a method invocation is ignore (when it potentially, shouldn't). + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class ClosureAllocationAnalyzer : DiagnosticAnalyzer + { + private const string DiagnosticId = DiagnosticIds.ClosureAllocation; + + private static readonly string Title = "Closure allocation."; + private static readonly string Message = "Closure allocation that captures {0}."; + + private static readonly string Description = "Closure allocation."; + private const string Category = "Performance"; + + // Using warning for visibility purposes + private const DiagnosticSeverity Severity = DiagnosticSeverity.Warning; + + private static readonly DiagnosticDescriptor ClosureAllocationRule = + new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, Severity, isEnabledByDefault: true, description: Description); + + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(ClosureAllocationRule); + + /// + public ClosureAllocationAnalyzer() + { + } + + /// + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + + context.RegisterOperationAction(AnalyzeAnonymousFunction, OperationKind.AnonymousFunction); + context.RegisterOperationAction(AnalyzeLocalFunction, OperationKind.LocalFunction); + } + + private void AnalyzeLocalFunction(OperationAnalysisContext context) + { + if (context.IsHiddenAllocationsAllowed()) + { + return; + } + + var operation = context.Operation; + var dataflow = operation.SemanticModel.AnalyzeDataFlow(operation.Syntax); + if (dataflow.Captured.Length != 0 && !dataflow.CapturesOnlyThis()) + { + // The local functions are different from anonymous methods. + // By default, the generated display "class" is actually a struct. + // But if the local function is converted to a delegate then the compiler + // generates a class. + + // (dco.Target as IMemberReferenceOperation).Member.Equals((context.Operation as ILocalFunctionOperation).Symbol) + var localFunctionSymbol = ((ILocalFunctionOperation) operation).Symbol; + var localFunctionToDelegateConversions = + operation.Parent.Descendants() + .Where(o => + o is IDelegateCreationOperation dco && dco.Target is IMemberReferenceOperation mr && + mr.Member.Equals(localFunctionSymbol)); + if (localFunctionToDelegateConversions.Any()) + { + string captures = string.Join(", ", dataflow.Captured.Select(c => $"'{c.Name}'")); + + Contract.Assert(operation.Parent.Syntax is BlockSyntax, "Local function's parent should be a block"); + var location = ((BlockSyntax)operation.Parent.Syntax).OpenBraceToken.GetLocation(); + + string message = + $"{captures} because local function '{localFunctionSymbol.Name}' is converted to a delegate"; + context.ReportDiagnostic(Diagnostic.Create(ClosureAllocationRule, location, message)); + } + } + } + + private void AnalyzeAnonymousFunction(OperationAnalysisContext context) + { + if (context.IsHiddenAllocationsAllowed()) + { + return; + } + + var operation = context.Operation; + + var dataflow = operation.SemanticModel.AnalyzeDataFlow(operation.Syntax); + if (dataflow.Captured.Length != 0) + { + if (dataflow.CapturesOnlyThis()) + { + // If the only captured variable is 'this' pointer, then + // we should not be emitting any diagnostics. + // Consider the following case: + // private static int s = 42; + // private int i = 42; + // public void M(int y) + // { + // // Delegate allocation, but no closure allocations. + // System.Func f = () => s + i; + // f(); + // } + // In this case, an anonymous method is generated directly in the enclosing type + // and not in the generate "closure" type. + // So in this case, the delegate is allocated, but there is no closure allocations. + return; + } + + // Current anonymous method captures external context + // Group captures per scope, because the compiler creates new display class instance + // per scope. + Dictionary> closuresPerScope = + dataflow.Captured + .Select(c => (scope: c.GetScopeForDisplayClass(), capture: c)) + .Where(tpl => tpl.scope != null) + // Grouping by the first element to create scope to symbols map. + .GroupToDictionary(); + + foreach (var kvp in closuresPerScope) + { + var locationSyntax = kvp.Key; + Location location = locationSyntax switch + { + BlockSyntax block => block.OpenBraceToken.GetLocation(), + ArrowExpressionClauseSyntax arrow => arrow.ArrowToken.GetLocation(), + ForEachStatementSyntax forEach => forEach.Statement is BlockSyntax block ? block.OpenBraceToken.GetLocation() : forEach.ForEachKeyword.GetLocation(), + _ => locationSyntax!.GetLocation(), + }; + + string captures = string.Join(", ", kvp.Value.Select(c => $"'{c.Name}'")); + + context.ReportDiagnostic(Diagnostic.Create(ClosureAllocationRule, location, captures)); + } + } + } + } +} \ No newline at end of file diff --git a/src/ErrorProne.NET.CoreAnalyzers/Allocations/DelegateAllocationAnalyzer.cs b/src/ErrorProne.NET.CoreAnalyzers/Allocations/DelegateAllocationAnalyzer.cs new file mode 100644 index 0000000..e0573da --- /dev/null +++ b/src/ErrorProne.NET.CoreAnalyzers/Allocations/DelegateAllocationAnalyzer.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.ContractsLight; +using System.Linq; +using ErrorProne.NET.AsyncAnalyzers; +using ErrorProne.NET.Core; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +#nullable enable + +namespace ErrorProne.NET.CoreAnalyzers.Allocations +{ + /// + /// Analyzer that warns when the result of a method invocation is ignore (when it potentially, shouldn't). + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class DelegateAllocationAnalyzer : DiagnosticAnalyzer + { + private const string Category = "Performance"; + private const DiagnosticSeverity Severity = DiagnosticSeverity.Warning; + + private const string DiagnosticId = DiagnosticIds.DelegateAllocation; + + private static readonly string Title = "Delegate allocation."; + private static readonly string Message = "Delegate allocation caused by {0}."; + + private static readonly string Description = "Delegate allocation."; + + private static readonly DiagnosticDescriptor Rule = + new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, Severity, isEnabledByDefault: true, description: Description); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + /// + public DelegateAllocationAnalyzer() + { + } + + /// + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.RegisterOperationAction(DelegateCreation, OperationKind.DelegateCreation); + context.RegisterOperationAction(AnalyzeAnonymousFunction, OperationKind.AnonymousFunction); + } + + private void DelegateCreation(OperationAnalysisContext context) + { + if (context.IsHiddenAllocationsAllowed()) + { + return; + } + + // This method is called for anonymous methods as well, + // but this method only handles method group conversion + // and the next method deals with delegate allocations. + // It is easier to do the rest there because we don't want + // to emit warnings for non-capturing anonymous methods. + if (context.Operation is IDelegateCreationOperation delegateCreation && delegateCreation.Target.Kind == OperationKind.MethodReference) + { + // This is a method group conversion. + context.ReportDiagnostic(Diagnostic.Create(Rule, context.Operation.Syntax.GetLocation(), "method group conversion")); + } + } + + private void AnalyzeAnonymousFunction(OperationAnalysisContext context) + { + if (context.IsHiddenAllocationsAllowed()) + { + return; + } + + // TODO: revisit this implementation! Maybe this can be done in DelegateCreation. + var operation = context.Operation; + + if (NoHiddenAllocationsConfiguration.ShouldNotDetectAllocationsFor(operation)) + { + return; + } + + var dataflow = operation.SemanticModel.AnalyzeDataFlow(operation.Syntax); + if (dataflow.Captured.Length != 0) + { + string? explanation = null; + if (dataflow.Captured.Length == 1 && dataflow.Captured[0] is IParameterSymbol ps && ps.IsThis) + { + // The only captured variable is 'this'. + // In this case the delegate is created (not cached) but a closure is not created/generated. + explanation = "capturing of 'this' reference (no display class is created)"; + } + else + { + var symbols = string.Join(", ", + dataflow.Captured.Where(c => !(c is IParameterSymbol ps && ps.IsThis)) + .Select(s => $"'{s.Name}'")); + + explanation = $"capturing of {symbols} (display class is created)"; + } + + var syntax = context.Operation.Syntax; + if (syntax != null) + { + Location location = syntax switch + { + LambdaExpressionSyntax ls => ls.ArrowToken.GetLocation(), + AnonymousMethodExpressionSyntax am => am.DelegateKeyword.GetLocation(), + _ => syntax.GetLocation(), + }; + + Contract.Assert(explanation != null); + context.ReportDiagnostic(Diagnostic.Create(Rule, location, explanation)); + } + } + // // Current anonymous method captures external context + // // Group captures per scope, because the compiler creates new display class instance + // // per scope. + + // Dictionary> closuresPerScope = + // dataflow.Captured + // .Select(c => (scope: c.GetScopeForDisplayClass(), capture: c)) + // .Where(tpl => tpl.scope != null) + // // Grouping by the first element to create scope to symbols map. + // .GroupToDictionary(); + + // foreach (var kvp in closuresPerScope) + // { + // var locationSyntax = kvp.Key; + // Location location = locationSyntax switch + // { + // BlockSyntax block => block.OpenBraceToken.GetLocation(), + // ArrowExpressionClauseSyntax arrow => arrow.ArrowToken.GetLocation(), + // ForEachStatementSyntax forEach => forEach.Statement is BlockSyntax block ? block.OpenBraceToken.GetLocation() : forEach.ForEachKeyword.GetLocation(), + // _ => locationSyntax!.GetLocation(), + // }; + + // string captures = string.Join(", ", kvp.Value.Select(c => $"'{c.Name}'")); + + // context.ReportDiagnostic(Diagnostic.Create(ClosureAllocationRule, location, captures)); + // } + //} + } + } +} \ No newline at end of file diff --git a/src/ErrorProne.NET.CoreAnalyzers/Allocations/ImplicitBoxingAllocationAnalyzer.cs b/src/ErrorProne.NET.CoreAnalyzers/Allocations/ImplicitBoxingAllocationAnalyzer.cs new file mode 100644 index 0000000..76e2d48 --- /dev/null +++ b/src/ErrorProne.NET.CoreAnalyzers/Allocations/ImplicitBoxingAllocationAnalyzer.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Immutable; +using ErrorProne.NET.AsyncAnalyzers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +#nullable enable + +namespace ErrorProne.NET.CoreAnalyzers.Allocations +{ + /// + /// Analyzer that warns when the result of a method invocation is ignore (when it potentially, shouldn't). + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed partial class ImplicitBoxingAllocationAnalyzer : DiagnosticAnalyzer + { + /// + public const string DiagnosticId = DiagnosticIds.ImplicitBoxing; + + private static readonly string Title = "Implicit boxing allocation."; + private static readonly string Message = "Implicit boxing allocation of type '{0}': {1}."; + + private static readonly string Description = "Implicit boxing allocation can cause performance issues if happened on application hot paths."; + private const string Category = "Performance"; + + // Using warning for visibility purposes + private const DiagnosticSeverity Severity = DiagnosticSeverity.Warning; + + /// + public static readonly DiagnosticDescriptor Rule = + new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, Severity, isEnabledByDefault: true, description: Description); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + /// + public ImplicitBoxingAllocationAnalyzer() + //: base(supportFading: false, diagnostics: Rule) + { + } + + /// + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + RegisterImplicitBoxingOperations(context); + + context.RegisterSyntaxNodeAction(AnalyzeInvocationExpression, SyntaxKind.InvocationExpression); + } + + private void AnalyzeInvocationExpression(SyntaxNodeAnalysisContext context) + { + if (context.ShouldNotDetectAllocationsFor()) + { + return; + } + + var invocation = (InvocationExpressionSyntax)context.Node; + + var targetSymbol = context.SemanticModel.GetSymbolInfo(invocation.Expression).Symbol; + + // Checking for foo.Bar() patterns that cause boxing allocations. + + if (targetSymbol != null && invocation.Expression is MemberAccessExpressionSyntax ms) + { + + var sourceOperation = context.SemanticModel.GetOperation(ms.Expression); + + if (sourceOperation?.Type != null) + { + if ( + // Boxing allocation occurs when the CLR calls a method for as struct that is defined in a value type. + // For instance, for non-overriden methods like ToString, GetHashCode, Equals + // or for calling methods defined in System.Enum type. + sourceOperation.Type.IsValueType + && targetSymbol.ContainingType?.IsValueType == false + + // Excluding the case when the method is called on generics. + && sourceOperation.Type.Kind != SymbolKind.TypeParameter + + // Excluding the case when a calling method is an extension method. + && !(targetSymbol is IMethodSymbol method && method.IsExtensionMethod) + // myStruct.GetType() is causing allocation only with 32-bits legacy jitter with full framework, + // and because this is the least used jitter (IMO) we decided exclude this case + // and not warn on it. + && targetSymbol.Name != nameof(GetType) + ) + { + string reason = $"calling an instance method inherited from {targetSymbol.ContainingType.ToDisplayString()}"; + if (targetSymbol.Name == nameof(Enum.HasFlag)) + { + reason += " (not applicable for Core CLR)"; + } + + context.ReportDiagnostic(Diagnostic.Create(Rule, ms.Name.GetLocation(), sourceOperation.Type.Name, reason)); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/ErrorProne.NET.CoreAnalyzers/Allocations/ImplicitCastBoxingAllocationAnalyzer.cs b/src/ErrorProne.NET.CoreAnalyzers/Allocations/ImplicitCastBoxingAllocationAnalyzer.cs new file mode 100644 index 0000000..2877427 --- /dev/null +++ b/src/ErrorProne.NET.CoreAnalyzers/Allocations/ImplicitCastBoxingAllocationAnalyzer.cs @@ -0,0 +1,117 @@ +using ErrorProne.NET.AsyncAnalyzers; +using ErrorProne.NET.Core; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +#nullable enable + +namespace ErrorProne.NET.CoreAnalyzers.Allocations +{ + partial class ImplicitBoxingAllocationAnalyzer + { + private void RegisterImplicitBoxingOperations(AnalysisContext context) + { + context.RegisterOperationAction(AnalyzeOperation, OperationKind.Conversion); + context.RegisterOperationAction(AnalyzeInterpolation, OperationKind.Interpolation); + context.RegisterSyntaxNodeAction(AnalyzeForEachLoop, SyntaxKind.ForEachStatement); + context.RegisterOperationAction(AnalyzeMethodReference, OperationKind.MethodReference); + } + + private void AnalyzeMethodReference(OperationAnalysisContext context) + { + if (NoHiddenAllocationsConfiguration.ShouldNotDetectAllocationsFor(context.Operation)) + { + return; + } + + var methodReference = (IMethodReferenceOperation)context.Operation; + + // This is a method group conversion from a struct's instance method. + if (methodReference.Instance?.Type?.IsValueType == true && !methodReference.Member.IsStatic) + { + context.ReportDiagnostic(Diagnostic.Create(Rule, methodReference.Instance.Syntax.GetLocation(), methodReference.Instance.Type.ToDisplayString(), "object")); + } + } + + private void AnalyzeOperation(OperationAnalysisContext context) + { + if (NoHiddenAllocationsConfiguration.ShouldNotDetectAllocationsFor(context.Operation)) + { + return; + } + + var conversion = (IConversionOperation)context.Operation; + + var targetType = conversion.Type; + var operandType = conversion.Operand.Type; + + if (conversion.IsImplicit && + operandType?.IsValueType == true && + targetType?.IsReferenceType == true + // User-defined conversions are fine: if they cause allocations then the conversion itself should be marked with NoHiddenAllocations. + && !conversion.Conversion.IsUserDefined) + { + context.ReportDiagnostic(Diagnostic.Create(Rule, conversion.Operand.Syntax.GetLocation(), operandType.ToDisplayString(), targetType.ToDisplayString())); + } + + if (conversion.IsImplicit && operandType?.IsTupleType == true && targetType?.IsTupleType == true) + { + var operandTypes = operandType.GetTupleTypes(); + var targetTypes = targetType.GetTupleTypes(); + + for (var i = 0; i < operandTypes.Length; i++) + { + if (operandTypes[i].IsValueType && targetTypes[i].IsReferenceType) + { + // This is the following case: + // (object, object) foo() => (1, 2); + context.ReportDiagnostic(Diagnostic.Create(Rule, conversion.Operand.Syntax.GetLocation(), operandType.ToDisplayString(), targetType.ToDisplayString())); + break; + } + } + } + } + + private void AnalyzeForEachLoop(SyntaxNodeAnalysisContext context) + { + if (NoHiddenAllocationsConfiguration.ShouldNotDetectAllocationsFor(context.Node, context.SemanticModel)) + { + return; + } + + // foreach loop can cause boxing allocation in the following case: + // foreach(object o in Enumerable.Range(1, 10)) + // In this case, all the elements are boxed to a target type. + + var foreachLoop = (IForEachLoopOperation)context.SemanticModel.GetOperation(context.Node); + + ITypeSymbol elementType = foreachLoop.GetElementType(); + if (elementType?.IsValueType == true && foreachLoop.GetConversionInfo()?.IsBoxing == true) + { + var targetTypeName = "Unknown"; + if (foreachLoop.LoopControlVariable is IVariableDeclaratorOperation op && op.Symbol.Type != null) + { + targetTypeName = op.Symbol.Type.ToDisplayString(); + } + + context.ReportDiagnostic(Diagnostic.Create(Rule, foreachLoop.Collection.Syntax.GetLocation(), elementType.ToDisplayString(), targetTypeName)); + } + } + + private void AnalyzeInterpolation(OperationAnalysisContext context) + { + if (NoHiddenAllocationsConfiguration.ShouldNotDetectAllocationsFor(context.Operation)) + { + return; + } + + // Covering cases when string interpolation is causing boxing, like $"{42}"; + if (context.Operation is IInterpolationOperation interpolationOperation && interpolationOperation.Expression.Type?.IsValueType == true) + { + context.ReportDiagnostic(Diagnostic.Create(Rule, interpolationOperation.Expression.Syntax.GetLocation(), interpolationOperation.Expression.Type.ToDisplayString(), "object")); + } + } + } +} \ No newline at end of file diff --git a/src/ErrorProne.NET.CoreAnalyzers/Allocations/ImplicitEnumeratorAllocationAnalyzer.cs b/src/ErrorProne.NET.CoreAnalyzers/Allocations/ImplicitEnumeratorAllocationAnalyzer.cs new file mode 100644 index 0000000..dadc5a4 --- /dev/null +++ b/src/ErrorProne.NET.CoreAnalyzers/Allocations/ImplicitEnumeratorAllocationAnalyzer.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.ContractsLight; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; +using ErrorProne.NET.AsyncAnalyzers; +using ErrorProne.NET.Core; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +#nullable enable + +namespace ErrorProne.NET.CoreAnalyzers.Allocations +{ + /// + /// Analyzer that warns when the result of a method invocation is ignore (when it potentially, shouldn't). + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class ImplicitEnumeratorAllocationAnalyzer : DiagnosticAnalyzer + { + /// + public const string DiagnosticId = DiagnosticIds.ImplicitEnumeratorBoxing; + + private static readonly string Title = "Boxing enumerator."; + private static readonly string Message = "Allocating or boxing enumerator of type {0}"; + + private static readonly string Description = "Return values of some methods should be observed."; + private const string Category = "CodeSmell"; + + // Using warning for visibility purposes + private const DiagnosticSeverity Severity = DiagnosticSeverity.Warning; + + /// + public static readonly DiagnosticDescriptor Rule = + new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, Severity, isEnabledByDefault: true, description: Description); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + /// + public ImplicitEnumeratorAllocationAnalyzer() + //: base(supportFading: false, diagnostics: Rule) + { + } + + /// + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(AnalyzeForeachStatement, SyntaxKind.ForEachStatement); + } + + private void AnalyzeForeachStatement(SyntaxNodeAnalysisContext context) + { + if (NoHiddenAllocationsConfiguration.ShouldNotDetectAllocationsFor(context.Node, context.SemanticModel)) + { + return; + } + + var foreachStatement = (ForEachStatementSyntax) context.Node; + + if (context.SemanticModel.GetOperation(foreachStatement) is IForEachLoopOperation foreachOperation) + { + if (foreachOperation.Collection.Type.SpecialType == SpecialType.System_String) + { + return; + } + + if (foreachOperation.Collection is IConversionOperation co && co.Operand?.Type is IArrayTypeSymbol) + { + // this is foreach over an array that is converted to for loop. + return; + } + + var getEnumeratorMethod = foreachOperation.GetEnumeratorMethod(); + if (getEnumeratorMethod?.ReturnType.IsValueType == false) + { + context.ReportDiagnostic(Diagnostic.Create(Rule, foreachStatement.Expression.GetLocation(), getEnumeratorMethod.ReturnType.Name)); + } + } + } + } +} \ No newline at end of file diff --git a/src/ErrorProne.NET.CoreAnalyzers/Allocations/LinqKnownMethodsAllocationAnalyzer.cs b/src/ErrorProne.NET.CoreAnalyzers/Allocations/LinqKnownMethodsAllocationAnalyzer.cs new file mode 100644 index 0000000..8a61fee --- /dev/null +++ b/src/ErrorProne.NET.CoreAnalyzers/Allocations/LinqKnownMethodsAllocationAnalyzer.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using ErrorProne.NET.AsyncAnalyzers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +#nullable enable + +namespace ErrorProne.NET.CoreAnalyzers.Allocations +{ + /// + /// Analyzer that warns when the result of a method invocation is ignore (when it potentially, shouldn't). + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class LinqKnownMethodsAllocationAnalyzer : DiagnosticAnalyzer + { + /// + public const string DiagnosticId = DiagnosticIds.LinqAllocation; + + private static readonly string Title = "LINQ method with known invocation."; + private static readonly string Message = "LINQ method {0} is known to cause allocations."; + + private static readonly string Description = "LINQ query is known to cause allocations."; + private const string Category = "CodeSmell"; + + // Using warning for visibility purposes + private const DiagnosticSeverity Severity = DiagnosticSeverity.Warning; + + /// + public static readonly DiagnosticDescriptor Rule = + new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, Severity, isEnabledByDefault: true, description: Description); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + /// + public LinqKnownMethodsAllocationAnalyzer() + { + } + + /// + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(AnalyzeInvocationExpression, SyntaxKind.InvocationExpression); + } + + private void AnalyzeInvocationExpression(SyntaxNodeAnalysisContext context) + { + if (NoHiddenAllocationsConfiguration.ShouldNotDetectAllocationsFor(context.Node, context.SemanticModel)) + { + return; + } + + var invocation = (InvocationExpressionSyntax)context.Node; + + var targetSymbol = context.SemanticModel.GetSymbolInfo(invocation.Expression).Symbol; + + if (targetSymbol?.ContainingType?.ToDisplayString() == "System.Linq.Enumerable" && invocation.Expression is MemberAccessExpressionSyntax ms) + { + if (targetSymbol.Name == "Count") + { + var sourceOperation = context.SemanticModel.GetOperation(ms.Expression); + + if (sourceOperation?.Type != null) + { + var interfaces = sourceOperation.Type.AllInterfaces; + foreach (var i in interfaces) + { + if (i.ToDisplayString() == "System.Collections.ICollection") + { + return; // ICollection.Count is optimized and does not cause allocations. + } + } + } + } + + context.ReportDiagnostic(Diagnostic.Create(Rule, ms.Name.GetLocation(), targetSymbol.ToDisplayString())); + } + } + } +} \ No newline at end of file diff --git a/src/ErrorProne.NET.CoreAnalyzers/Allocations/RecursiveNoHiddenAllocationAttributeAnalyzer.cs b/src/ErrorProne.NET.CoreAnalyzers/Allocations/RecursiveNoHiddenAllocationAttributeAnalyzer.cs new file mode 100644 index 0000000..b76babc --- /dev/null +++ b/src/ErrorProne.NET.CoreAnalyzers/Allocations/RecursiveNoHiddenAllocationAttributeAnalyzer.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using ErrorProne.NET.AsyncAnalyzers; +using ErrorProne.NET.Core; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +#nullable enable + +namespace ErrorProne.NET.CoreAnalyzers.Allocations +{ + /// + /// Analyzer that warns when the result of a method invocation is ignore (when it potentially, shouldn't). + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class RecursiveNoHiddenAllocationAttributeAnalyzer : DiagnosticAnalyzer + { + /// + public const string DiagnosticId = DiagnosticIds.LinqAllocation; + + private static readonly string Title = "Recursive hidden application enforcement."; + private static readonly string Message = "{0} \"{1}\" is being called from within a recursive NoHiddenAllocations region and is not marked with NoHiddenAllocations."; + + private static readonly string Description = "A region which is marked with NoHiddenAllocations(Recursive = true) requires its callers to also be marked with the NoHiddenAllocations attribute."; + private const string Category = "CodeSmell"; + + // Using warning for visibility purposes + private const DiagnosticSeverity Severity = DiagnosticSeverity.Warning; + + /// + public static readonly DiagnosticDescriptor Rule = + new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, Severity, isEnabledByDefault: true, description: Description); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + /// + public RecursiveNoHiddenAllocationAttributeAnalyzer() + { + } + + /// + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + + context.RegisterOperationAction(AnalyzeInvocationOperation, OperationKind.Invocation); + context.RegisterOperationAction(AnalyzePropertyReference, OperationKind.PropertyReference); + + context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation); + } + + private void AnalyzeObjectCreation(OperationAnalysisContext context) + { + if (NoHiddenAllocationsConfiguration.ShouldNotEnforceRecursiveApplication(context.Operation)) + { + return; + } + + var operation = (IObjectCreationOperation)context.Operation; + + if (operation.Constructor.IsImplicitlyDeclared) + { + return; + } + + // do not trigger for library code + if (operation.Constructor.ContainingType.DeclaringSyntaxReferences.Length == 0) + { + return; + } + + if (NoHiddenAllocationsConfiguration.ShouldNotDetectAllocationsFor(operation.Constructor)) + { + context.ReportDiagnostic(Diagnostic.Create(Rule, operation.Syntax.GetLocation(), "Constructor for", operation.Type.Name)); + } + } + + private void AnalyzePropertyReference(OperationAnalysisContext context) + { + if (NoHiddenAllocationsConfiguration.ShouldNotEnforceRecursiveApplication(context.Operation)) + { + return; + } + + var operation = (IPropertyReferenceOperation)context.Operation; + + var propertySymbol = operation.Property; + + var propertyDeclaration = propertySymbol.TryGetDeclarationSyntax(); + + // Can't figure out how to easily tell if it's a getter or a setter being called, so only check for attributes at the property level. + // This effectively ignores attributes at the getter / setter level. + if (propertyDeclaration != null && NoHiddenAllocationsConfiguration.ShouldNotDetectAllocationsFor(propertyDeclaration, operation.SemanticModel)) + { + context.ReportDiagnostic(Diagnostic.Create(Rule, operation.Syntax.GetLocation(), "Property", propertySymbol.Name)); + } + } + + private void AnalyzeInvocationOperation(OperationAnalysisContext context) + { + if (NoHiddenAllocationsConfiguration.ShouldNotEnforceRecursiveApplication(context.Operation)) + { + return; + } + + var operation = ((IInvocationOperation) context.Operation); + var targetSyntax = operation.TargetMethod.TryGetDeclarationSyntax(); + + if (targetSyntax != null && NoHiddenAllocationsConfiguration.ShouldNotDetectAllocationsFor(targetSyntax, context.Operation.SemanticModel)) + { + context.ReportDiagnostic(Diagnostic.Create(Rule, operation.Syntax.GetLocation(), "Method", operation.TargetMethod.Name)); + } + } + } +} \ No newline at end of file diff --git a/src/ErrorProne.NET.CoreAnalyzers/AsyncAnalyzers/AnalysisActivation.cs b/src/ErrorProne.NET.CoreAnalyzers/AsyncAnalyzers/AnalysisActivation.cs new file mode 100644 index 0000000..a8776cb --- /dev/null +++ b/src/ErrorProne.NET.CoreAnalyzers/AsyncAnalyzers/AnalysisActivation.cs @@ -0,0 +1,242 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.ContractsLight; +using System.Linq; +using ErrorProne.NET.Core; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +#nullable enable + +[assembly:NoHiddenAllocations] public sealed class NoHiddenAllocationsAttribute : System.Attribute +public sealed class NoHiddenAllocationsAttribute : System.Attribute +{ + public bool Recursive; +} +namespace ErrorProne.NET.AsyncAnalyzers +{ + public static class NoHiddenAllocationsConfiguration + { + public enum NoHiddenAllocationsLevel + { + Default, + Recursive, + } + + private static string AttributeName = "NoHiddenAllocations"; + + public static bool ShouldNotDetectAllocationsFor(SyntaxNode node, SemanticModel semanticModel) + { + Contract.Requires(node != null); + Contract.Requires(semanticModel != null); + + return TryGetConfiguration(node, semanticModel) == null; + } + + public static bool ShouldNotDetectAllocationsFor(IOperation operation) + { + Contract.Requires(operation != null); + + return TryGetConfiguration(operation) == null; + } + + public static bool ShouldNotDetectAllocationsFor(this SyntaxNodeAnalysisContext context) + { + return ShouldNotDetectAllocationsFor(context.Node, context.SemanticModel); + } + + public static bool ShouldNotDetectAllocationsFor(ISymbol symbol) + { + Contract.Requires(symbol != null); + + return TryGetAllocationLevelFromSymbolOrAncestors(symbol, out _) == false; + } + + public static bool ShouldNotEnforceRecursiveApplication(IOperation operation) + { + Contract.Requires(operation != null); + + return TryGetConfiguration(operation) != NoHiddenAllocationsLevel.Recursive; + } + + private static NoHiddenAllocationsLevel? TryGetConfiguration(IOperation operation) + { + return TryGetConfiguration(operation.Syntax, operation.SemanticModel); + } + + public static bool IsHiddenAllocationsAllowed(this OperationAnalysisContext context) + { + return TryGetConfiguration(context.Operation.Syntax, context.Operation.SemanticModel) == null; + } + + public static NoHiddenAllocationsLevel? TryGetConfiguration(SyntaxNode node, SemanticModel semanticModel) + { + // The assembly can have the attribute, or any of the node's ancestors + if (TryGetAllocationLevel(semanticModel.Compilation.Assembly.GetAttributes(), AttributeName, out var allocationLevel)) + { + return allocationLevel; + } + + var operation = semanticModel.GetOperation(node); + + // Try and find an enclosing method declaration + var enclosingMethodBodyOperation = + operation?.AncestorAndSelf().FirstOrDefault(o => o is IMethodBodyBaseOperation); + + if (enclosingMethodBodyOperation != null) + { + // Property getter / setter with block and attribute on property + if (enclosingMethodBodyOperation.Syntax is AccessorDeclarationSyntax accessorDeclaration) + { + var propertyDeclarationSyntax = accessorDeclaration.Ancestors().FirstOrDefault(a => a is PropertyDeclarationSyntax); + if (propertyDeclarationSyntax != null) + { + var propertySymbol = semanticModel.GetDeclaredSymbol(propertyDeclarationSyntax); + + if (TryGetAllocationLevel(propertySymbol?.GetAttributes(), AttributeName, out allocationLevel)) + { + return allocationLevel; + } + } + } + + var symbol = semanticModel.GetDeclaredSymbol(enclosingMethodBodyOperation.Syntax); + + if (TryGetAllocationLevelFromSymbolOrAncestors(symbol, out allocationLevel)) + { + return allocationLevel; + } + } + + // Property with arrow blocks (either automatic property with default or arrow based getter / setter) with attribute on property + var enclosingArrowBlock = + operation?.AncestorAndSelf().FirstOrDefault(o => o is IBlockOperation && o.Syntax is ArrowExpressionClauseSyntax); + + if (enclosingArrowBlock != null) + { + // Need to get a property declaration in this case for Arrow-based property + var symbol = semanticModel.GetDeclaredSymbol(GetPropertyDeclarationSyntax((ArrowExpressionClauseSyntax) enclosingArrowBlock.Syntax)); + + if (TryGetAllocationLevelFromSymbolOrAncestors(symbol, out allocationLevel)) + { + return allocationLevel; + } + } + + // node could be a property declaration syntax, which does not have operations + if (operation == null && node is PropertyDeclarationSyntax propertySyntax) + { + var symbol = semanticModel.GetDeclaredSymbol(propertySyntax); + + if (TryGetAllocationLevelFromSymbolOrAncestors(symbol, out allocationLevel)) + { + return allocationLevel; + } + } + + return null; + } + + private static bool TryGetAllocationLevelFromSymbolOrAncestors(ISymbol symbol, out NoHiddenAllocationsLevel? allocationLevel) + { + allocationLevel = null; + + if (symbol == null) + { + return false; + } + + foreach (var containingSymbol in symbol.GetContainingSymbolsAndSelf()) + { + if (TryGetAllocationLevel(containingSymbol.GetAttributes(), AttributeName, out var level)) + { + allocationLevel = level; + return true; + } + } + + return false; + } + + private static SyntaxNode GetPropertyDeclarationSyntax(ArrowExpressionClauseSyntax node) + { + return node.AncestorsAndSelf().FirstOrDefault(a => a is PropertyDeclarationSyntax); + } + + private static bool TryGetAllocationLevel( + ImmutableArray? attributes, + string attributeName, + out NoHiddenAllocationsLevel? allocationLevel) + { + allocationLevel = null; + + var attribute = (attributes?.Where(a => a.AttributeClass.Name.StartsWith(attributeName)) ?? Enumerable.Empty()).FirstOrDefault(); + + if (attribute == null) + { + allocationLevel = null; + return false; + } + + allocationLevel = attribute.NamedArguments.Any(kvp => kvp.Key.Equals("Recursive") && kvp.Value.Value.Equals(true)) + ? NoHiddenAllocationsLevel.Recursive + : NoHiddenAllocationsLevel.Default; + + return true; + } + + private static IEnumerable AncestorAndSelf(this IOperation operation) + { + while (operation != null) + { + yield return operation; + operation = operation.Parent; + } + } + + private static IEnumerable GetContainingSymbolsAndSelf(this ISymbol symbol) + { + while (symbol != null) + { + yield return symbol; + symbol = symbol.ContainingSymbol; + } + } + } + + public static class ConfigureAwaitConfiguration + { + public static ConfigureAwait? TryGetConfigureAwait(Compilation compilation) + { + var attributes = compilation.Assembly.GetAttributes(); + if (attributes.Any(a => a.AttributeClass.Name.StartsWith("DoNotUseConfigureAwait"))) + { + return ConfigureAwait.DoNotUseConfigureAwait; + } + + if (attributes.Any(a => a.AttributeClass.Name.StartsWith("UseConfigureAwaitFalse"))) + { + return ConfigureAwait.UseConfigureAwaitFalse; + } + + + return null; + } + } + + public enum ConfigureAwait + { + UseConfigureAwaitFalse, + DoNotUseConfigureAwait, + } + + internal static class TempExtensions + { + public static bool IsConfigureAwait(this IMethodSymbol method, Compilation compilation) + { + // Naive implementation + return method.Name == "ConfigureAwait" && method.ReceiverType.IsTaskLike(compilation); + } + } +} diff --git a/src/ErrorProne.NET.CoreAnalyzers/AsyncAnalyzers/ConfigureAwaitConfiguration.cs b/src/ErrorProne.NET.CoreAnalyzers/AsyncAnalyzers/ConfigureAwaitConfiguration.cs deleted file mode 100644 index f9fe15b..0000000 --- a/src/ErrorProne.NET.CoreAnalyzers/AsyncAnalyzers/ConfigureAwaitConfiguration.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Linq; -using ErrorProne.NET.Core; -using Microsoft.CodeAnalysis; - -namespace ErrorProne.NET.AsyncAnalyzers -{ - public static class ConfigureAwaitConfiguration - { - public static ConfigureAwait? TryGetConfigureAwait(Compilation compilation) - { - var attributes = compilation.Assembly.GetAttributes(); - if (attributes.Any(a => a.AttributeClass.Name.StartsWith("DoNotUseConfigureAwait"))) - { - return ConfigureAwait.DoNotUseConfigureAwait; - } - - if (attributes.Any(a => a.AttributeClass.Name.StartsWith("UseConfigureAwaitFalse"))) - { - return ConfigureAwait.UseConfigureAwaitFalse; - } - - - return null; - } - } - - public enum ConfigureAwait - { - UseConfigureAwaitFalse, - DoNotUseConfigureAwait, - } - - - internal static class TempExtensions - { - public static bool IsConfigureAwait(this IMethodSymbol method, Compilation compilation) - { - // Naive implementation - return method.Name == "ConfigureAwait" && method.ReceiverType.IsTaskLike(compilation); - } - } -} \ No newline at end of file diff --git a/src/ErrorProne.NET.CoreAnalyzers/DiagnosticIds.cs b/src/ErrorProne.NET.CoreAnalyzers/DiagnosticIds.cs index 36b381d..59e33df 100644 --- a/src/ErrorProne.NET.CoreAnalyzers/DiagnosticIds.cs +++ b/src/ErrorProne.NET.CoreAnalyzers/DiagnosticIds.cs @@ -27,5 +27,12 @@ public static class DiagnosticIds public const string IncorrectExceptionPropagation = "ERP021"; public const string AllExceptionSwalled = "ERP022"; public const string OnlyExceptionMessageWasObserved = "ERP023"; + + // Allocations + public const string ImplicitBoxing = "ERP031"; + public const string ImplicitEnumeratorBoxing = "ERP032"; + public const string ClosureAllocation = "ERP034"; + public const string DelegateAllocation = "ERP035"; + public const string LinqAllocation = "ERP036"; } } \ No newline at end of file diff --git a/src/ErrorProne.NET.CoreAnalyzers/ErrorProne.NET.CoreAnalyzers.csproj b/src/ErrorProne.NET.CoreAnalyzers/ErrorProne.NET.CoreAnalyzers.csproj index 112c17b..59c3094 100644 --- a/src/ErrorProne.NET.CoreAnalyzers/ErrorProne.NET.CoreAnalyzers.csproj +++ b/src/ErrorProne.NET.CoreAnalyzers/ErrorProne.NET.CoreAnalyzers.csproj @@ -1,22 +1,25 @@  - netstandard1.3 + netstandard2.0 ErrorProne.Net.CoreAnalyzers ErrorProne.NET + + preview + + ErrorProne.NET.CoreAnalyzers.RenameToAvoidConflict - + - diff --git a/src/ErrorProne.NET.StructAnalyzers.CodeFixes/ErrorProne.NET.StructAnalyzers.CodeFixes.csproj b/src/ErrorProne.NET.StructAnalyzers.CodeFixes/ErrorProne.NET.StructAnalyzers.CodeFixes.csproj index 5015dec..36c1be4 100644 --- a/src/ErrorProne.NET.StructAnalyzers.CodeFixes/ErrorProne.NET.StructAnalyzers.CodeFixes.csproj +++ b/src/ErrorProne.NET.StructAnalyzers.CodeFixes/ErrorProne.NET.StructAnalyzers.CodeFixes.csproj @@ -1,7 +1,7 @@  - netstandard1.3 + netstandard2.0 ErrorProne.Net.StructAnalyzers.CodeFixes ErrorProne.Net.StructAnalyzers false @@ -49,6 +49,8 @@ + + diff --git a/src/ErrorProne.NET.StructAnalyzers.Tests/ErrorProne.NET.StructAnalyzers.Tests.csproj b/src/ErrorProne.NET.StructAnalyzers.Tests/ErrorProne.NET.StructAnalyzers.Tests.csproj index 485c282..9168796 100644 --- a/src/ErrorProne.NET.StructAnalyzers.Tests/ErrorProne.NET.StructAnalyzers.Tests.csproj +++ b/src/ErrorProne.NET.StructAnalyzers.Tests/ErrorProne.NET.StructAnalyzers.Tests.csproj @@ -5,8 +5,8 @@ - - + + diff --git a/src/ErrorProne.NET.StructAnalyzers/ErrorProne.NET.StructAnalyzers.csproj b/src/ErrorProne.NET.StructAnalyzers/ErrorProne.NET.StructAnalyzers.csproj index a7c1ef6..db36b49 100644 --- a/src/ErrorProne.NET.StructAnalyzers/ErrorProne.NET.StructAnalyzers.csproj +++ b/src/ErrorProne.NET.StructAnalyzers/ErrorProne.NET.StructAnalyzers.csproj @@ -1,13 +1,13 @@  - netstandard1.3 + netstandard2.0 ErrorProne.Net.StructAnalyzers ErrorProne.Net.StructAnalyzers - + diff --git a/src/ErrorProne.NET.Vsix/ErrorProne.NET.Vsix.csproj b/src/ErrorProne.NET.Vsix/ErrorProne.NET.Vsix.csproj index 4bd4fc8..66e5f9b 100644 --- a/src/ErrorProne.NET.Vsix/ErrorProne.NET.Vsix.csproj +++ b/src/ErrorProne.NET.Vsix/ErrorProne.NET.Vsix.csproj @@ -3,7 +3,7 @@ - net46 + net472 ErrorProne.NET @@ -35,7 +35,7 @@ - + diff --git a/src/RoslynNunitTestRunner/CodeFixTestExtensions.cs b/src/RoslynNunitTestRunner/CodeFixTestExtensions.cs index e35f2bd..0cdbf4a 100644 --- a/src/RoslynNunitTestRunner/CodeFixTestExtensions.cs +++ b/src/RoslynNunitTestRunner/CodeFixTestExtensions.cs @@ -37,5 +37,56 @@ internal sealed class DoNotUseConfigureAwaitAttribute : System.Attribute { } return test; } + + public static TTest WithHiddenAllocationsAttributeDeclaration(this TTest test) + where TTest : CodeFixTest + { + (string filename, string content) file = ("HiddenAllocationAttributes.cs", @"// +[System.AttributeUsage( + System.AttributeTargets.Assembly | System.AttributeTargets.Class | System.AttributeTargets.Method | System.AttributeTargets.Constructor |System.AttributeTargets.Property, + AllowMultiple = false, + Inherited = true +)] +internal sealed class NoHiddenAllocations : System.Attribute { + public bool Recursive; +} +"); + + test.TestState.Sources.Add(file); + + if (test.FixedState.Sources.Count > 0) + { + test.FixedState.Sources.Add(file); + } + + if (test.BatchFixedState.Sources.Count > 0) + { + test.BatchFixedState.Sources.Add(file); + } + + return test; + } + + public static TTest WithAssemblyLevelHiddenAllocationsAttribute(this TTest test) + where TTest : CodeFixTest + { + (string filename, string content) file = ("HiddenAllocationAttributes.cs", @"// +[assembly: NoHiddenAllocations] +"); + + test.TestState.Sources.Add(file); + + if (test.FixedState.Sources.Count > 0) + { + test.FixedState.Sources.Add(file); + } + + if (test.BatchFixedState.Sources.Count > 0) + { + test.BatchFixedState.Sources.Add(file); + } + + return test; + } } } diff --git a/src/RoslynNunitTestRunner/RoslynNunitTestRunner.csproj b/src/RoslynNunitTestRunner/RoslynNunitTestRunner.csproj index 4ae34cf..783f2eb 100644 --- a/src/RoslynNunitTestRunner/RoslynNunitTestRunner.csproj +++ b/src/RoslynNunitTestRunner/RoslynNunitTestRunner.csproj @@ -1,11 +1,11 @@  - netstandard1.6 + netstandard2.0 - +