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
-
-
+
+
+
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
-
+