diff --git a/ArchUnit.sln b/ArchUnit.sln
index 5f7c40a35..1c6cf1d57 100644
--- a/ArchUnit.sln
+++ b/ArchUnit.sln
@@ -65,6 +65,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClassAssembly", "TestAssemb
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlicesTestAssembly", "TestAssemblies\SlicesTestAssembly\SlicesTestAssembly.csproj", "{E421FA12-BBB9-4A8B-BD29-8757836079A5}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OptimizedAssembly", "TestAssemblies\OptimizedAssembly\OptimizedAssembly.csproj", "{74857E33-09C5-43C7-AA27-E9373DEC1380}"
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A780B3D6-C525-4270-ABF1-63BA65EA1593}"
ProjectSection(SolutionItems) = preProject
README.md = README.md
@@ -441,6 +443,18 @@ Global
{E421FA12-BBB9-4A8B-BD29-8757836079A5}.Release|x64.Build.0 = Release|Any CPU
{E421FA12-BBB9-4A8B-BD29-8757836079A5}.Release|x86.ActiveCfg = Release|Any CPU
{E421FA12-BBB9-4A8B-BD29-8757836079A5}.Release|x86.Build.0 = Release|Any CPU
+ {74857E33-09C5-43C7-AA27-E9373DEC1380}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {74857E33-09C5-43C7-AA27-E9373DEC1380}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {74857E33-09C5-43C7-AA27-E9373DEC1380}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {74857E33-09C5-43C7-AA27-E9373DEC1380}.Debug|x64.Build.0 = Debug|Any CPU
+ {74857E33-09C5-43C7-AA27-E9373DEC1380}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {74857E33-09C5-43C7-AA27-E9373DEC1380}.Debug|x86.Build.0 = Debug|Any CPU
+ {74857E33-09C5-43C7-AA27-E9373DEC1380}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {74857E33-09C5-43C7-AA27-E9373DEC1380}.Release|Any CPU.Build.0 = Release|Any CPU
+ {74857E33-09C5-43C7-AA27-E9373DEC1380}.Release|x64.ActiveCfg = Release|Any CPU
+ {74857E33-09C5-43C7-AA27-E9373DEC1380}.Release|x64.Build.0 = Release|Any CPU
+ {74857E33-09C5-43C7-AA27-E9373DEC1380}.Release|x86.ActiveCfg = Release|Any CPU
+ {74857E33-09C5-43C7-AA27-E9373DEC1380}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -459,6 +473,7 @@ Global
{1D0187EB-9D04-4CF3-AE63-0C0E97FCB49C} = {B1191F18-91CB-4387-B775-A5EB64D3AC30}
{2C04EB93-5AE0-474C-B328-145A734A5E2B} = {B1191F18-91CB-4387-B775-A5EB64D3AC30}
{E421FA12-BBB9-4A8B-BD29-8757836079A5} = {B1191F18-91CB-4387-B775-A5EB64D3AC30}
+ {74857E33-09C5-43C7-AA27-E9373DEC1380} = {B1191F18-91CB-4387-B775-A5EB64D3AC30}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D1A8274D-32D6-44DB-9BB3-1A5B273709AF}
diff --git a/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs b/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs
index 061dc5a28..654a9c8eb 100644
--- a/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs
+++ b/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs
@@ -289,6 +289,32 @@ internal static bool IsAsync(this MethodDefinition methodDefinition)
);
}
+ ///
+ /// Resolves the compiler-generated state machine type of an async or iterator method
+ /// from its or
+ /// . Unlike
+ /// looking for the newobj that creates it, this also works for the struct state
+ /// machines the compiler emits for async methods in optimized builds.
+ ///
+ [CanBeNull]
+ internal static TypeDefinition GetStateMachineType(this MethodDefinition methodDefinition)
+ {
+ var stateMachineAttribute = methodDefinition.CustomAttributes.FirstOrDefault(att =>
+ att.AttributeType.FullName
+ == typeof(System.Runtime.CompilerServices.AsyncStateMachineAttribute).FullName
+ || att.AttributeType.FullName
+ == typeof(System.Runtime.CompilerServices.IteratorStateMachineAttribute).FullName
+ );
+ if (stateMachineAttribute == null || !stateMachineAttribute.HasConstructorArguments)
+ {
+ return null;
+ }
+
+ var stateMachineType =
+ stateMachineAttribute.ConstructorArguments[0].Value as TypeReference;
+ return stateMachineType?.Resolve();
+ }
+
internal static bool IsCompilerGenerated(this MemberReference memberReference)
{
if (memberReference.Name.HasCompilerGeneratedName())
diff --git a/ArchUnitNET/Loader/TypeProcessor.cs b/ArchUnitNET/Loader/TypeProcessor.cs
index c765cdd8a..193812176 100644
--- a/ArchUnitNET/Loader/TypeProcessor.cs
+++ b/ArchUnitNET/Loader/TypeProcessor.cs
@@ -864,23 +864,18 @@ private static void HandleIterator(
DomainResolver domainResolver
)
{
- var compilerGeneratedGeneratorObject = methodBody
- .Instructions.Where(inst => inst.IsNewObjectOp())
- .Select(inst => ((MethodReference)inst.Operand).DeclaringType.Resolve())
- .FirstOrDefault(type =>
- type != null
- && type.Methods.Any(method => method.Name == nameof(IEnumerator.MoveNext))
- );
+ var compilerGeneratedGeneratorObject = methodBody.Method.GetStateMachineType();
+ var moveNextMethod = compilerGeneratedGeneratorObject?.Methods.FirstOrDefault(method =>
+ method.Name == nameof(IEnumerator.MoveNext)
+ );
- if (compilerGeneratedGeneratorObject == null)
+ if (moveNextMethod == null)
{
methodDefinition = methodBody.Method;
return;
}
- methodDefinition = compilerGeneratedGeneratorObject.Methods.First(method =>
- method.Name == nameof(IEnumerator.MoveNext)
- );
+ methodDefinition = moveNextMethod;
visitedMethodReferences.Add(methodDefinition);
methodBody = methodDefinition.Body;
@@ -909,25 +904,18 @@ private static void HandleAsync(
DomainResolver domainResolver
)
{
- var compilerGeneratedGeneratorObject = methodBody
- .Instructions.Where(inst => inst.IsNewObjectOp())
- .Select(inst => ((MethodReference)inst.Operand).DeclaringType.Resolve())
- .FirstOrDefault(type =>
- type != null
- && type.Methods.Any(method =>
- method.Name == nameof(IAsyncStateMachine.MoveNext)
- )
- );
+ var compilerGeneratedGeneratorObject = methodBody.Method.GetStateMachineType();
+ var moveNextMethod = compilerGeneratedGeneratorObject?.Methods.FirstOrDefault(method =>
+ method.Name == nameof(IAsyncStateMachine.MoveNext)
+ );
- if (compilerGeneratedGeneratorObject == null)
+ if (moveNextMethod == null)
{
methodDefinition = methodBody.Method;
return;
}
- methodDefinition = compilerGeneratedGeneratorObject.Methods.First(method =>
- method.Name == nameof(IAsyncStateMachine.MoveNext)
- );
+ methodDefinition = moveNextMethod;
visitedMethodReferences.Add(methodDefinition);
methodBody = methodDefinition.Body;
diff --git a/ArchUnitNETTests/ArchUnitNETTests.csproj b/ArchUnitNETTests/ArchUnitNETTests.csproj
index c56d20a5a..8528cec9c 100644
--- a/ArchUnitNETTests/ArchUnitNETTests.csproj
+++ b/ArchUnitNETTests/ArchUnitNETTests.csproj
@@ -36,6 +36,7 @@
+
diff --git a/ArchUnitNETTests/Dependencies/OptimizedStateMachineDependenciesTests.cs b/ArchUnitNETTests/Dependencies/OptimizedStateMachineDependenciesTests.cs
new file mode 100644
index 000000000..7dcbe2b19
--- /dev/null
+++ b/ArchUnitNETTests/Dependencies/OptimizedStateMachineDependenciesTests.cs
@@ -0,0 +1,79 @@
+using System.Linq;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using ArchUnitNET.Domain;
+using ArchUnitNET.Domain.Extensions;
+using OptimizedAssembly;
+using Xunit;
+using static ArchUnitNET.Fluent.ArchRuleDefinition;
+
+namespace ArchUnitNETTests.Dependencies
+{
+ ///
+ /// Async and iterator methods in an optimized build. There the compiler emits the async
+ /// state machine as a struct, which is initialized in place rather than created with
+ /// newobj, so it has to be found through the method's state machine attribute.
+ ///
+ public class OptimizedStateMachineDependenciesTests
+ {
+ private static readonly Architecture Architecture =
+ StaticTestArchitectures.OptimizedArchitecture;
+
+ private readonly MethodMember _calledMethod;
+
+ public OptimizedStateMachineDependenciesTests()
+ {
+ _calledMethod = Architecture
+ .GetClassOfType(typeof(CalledClass))
+ .GetMethodMembersWithName(nameof(CalledClass.CalledMethod) + "()")
+ .First();
+ }
+
+ [Fact]
+ public void AsyncStateMachineIsAStruct()
+ {
+ var stateMachineType = typeof(ClassWithAsyncMethod)
+ .GetMethod(nameof(ClassWithAsyncMethod.AsyncMethod))
+ .GetCustomAttribute()
+ .StateMachineType;
+
+ Assert.True(stateMachineType.IsValueType);
+ }
+
+ [Fact]
+ public void AsyncMethodBodyDependenciesAreFound()
+ {
+ var asyncMethod = Architecture
+ .GetClassOfType(typeof(ClassWithAsyncMethod))
+ .GetMethodMembersWithName(nameof(ClassWithAsyncMethod.AsyncMethod) + "()")
+ .First();
+
+ Assert.Contains(_calledMethod, asyncMethod.GetCalledMethods());
+
+ var rule = Classes()
+ .That()
+ .HaveFullName(typeof(ClassWithAsyncMethod).FullName)
+ .Should()
+ .NotDependOnAny(typeof(CalledClass));
+ Assert.False(rule.HasNoViolations(Architecture));
+ }
+
+ [Fact]
+ public void IteratorMethodBodyDependenciesAreFound()
+ {
+ var iteratorMethod = Architecture
+ .GetClassOfType(typeof(ClassWithIteratorMethod))
+ .GetMethodMembersWithName(nameof(ClassWithIteratorMethod.IteratorMethod) + "()")
+ .First();
+
+ Assert.Contains(_calledMethod, iteratorMethod.GetCalledMethods());
+
+ var rule = Classes()
+ .That()
+ .HaveFullName(typeof(ClassWithIteratorMethod).FullName)
+ .Should()
+ .NotDependOnAny(typeof(CalledClass));
+ Assert.False(rule.HasNoViolations(Architecture));
+ }
+ }
+}
diff --git a/ArchUnitNETTests/Domain/Dependencies/Members/MethodCallDependencyTests.cs b/ArchUnitNETTests/Domain/Dependencies/Members/MethodCallDependencyTests.cs
index b6b1c7796..6e0ed9a7f 100644
--- a/ArchUnitNETTests/Domain/Dependencies/Members/MethodCallDependencyTests.cs
+++ b/ArchUnitNETTests/Domain/Dependencies/Members/MethodCallDependencyTests.cs
@@ -96,7 +96,7 @@ MethodCallDependency expectedDependency
Assert.Contains(expectedDependency, originMember.GetMethodCallDependencies());
}
- [SkipInReleaseBuildTheory]
+ [Theory]
[ClassData(typeof(MethodDependencyTestBuild.MethodCallDependencyInAsyncMethodTestData))]
public void MethodCallDependenciesAreFoundInAsyncMethod(
IMember originMember,
diff --git a/ArchUnitNETTests/SkipInReleaseBuild.cs b/ArchUnitNETTests/SkipInReleaseBuild.cs
index de07925c5..3d5e150b2 100644
--- a/ArchUnitNETTests/SkipInReleaseBuild.cs
+++ b/ArchUnitNETTests/SkipInReleaseBuild.cs
@@ -8,16 +8,6 @@ public SkipInReleaseBuild()
{
#if !DEBUG
Skip = "This test only works in debug build";
-#endif
- }
- }
-
- public sealed class SkipInReleaseBuildTheory : TheoryAttribute
- {
- public SkipInReleaseBuildTheory()
- {
-#if !DEBUG
- Skip = "This test only works in debug build";
#endif
}
}
diff --git a/ArchUnitNETTests/StaticTestArchitectures.cs b/ArchUnitNETTests/StaticTestArchitectures.cs
index b9dcef78b..7b49790f7 100644
--- a/ArchUnitNETTests/StaticTestArchitectures.cs
+++ b/ArchUnitNETTests/StaticTestArchitectures.cs
@@ -102,6 +102,10 @@ public static class StaticTestArchitectures
)
.Build();
+ public static readonly Architecture OptimizedArchitecture = new ArchLoader()
+ .LoadAssemblies(typeof(OptimizedAssembly.ClassWithAsyncMethod).Assembly)
+ .Build();
+
public static readonly Architecture ArchUnitNETTestAssemblyArchitecture = new ArchLoader()
.LoadAssemblies(typeof(Class1).Assembly)
.Build();
diff --git a/TestAssemblies/OptimizedAssembly/OptimizedAssembly.csproj b/TestAssemblies/OptimizedAssembly/OptimizedAssembly.csproj
new file mode 100644
index 000000000..19fc7471b
--- /dev/null
+++ b/TestAssemblies/OptimizedAssembly/OptimizedAssembly.csproj
@@ -0,0 +1,15 @@
+
+
+ net10.0
+ enable
+ enable
+ true
+ false
+ True
+ ..\..\strongNameKey.snk
+ 1.0.0.0
+
+ true
+
+
diff --git a/TestAssemblies/OptimizedAssembly/StateMachineClasses.cs b/TestAssemblies/OptimizedAssembly/StateMachineClasses.cs
new file mode 100644
index 000000000..03b54ed0b
--- /dev/null
+++ b/TestAssemblies/OptimizedAssembly/StateMachineClasses.cs
@@ -0,0 +1,24 @@
+namespace OptimizedAssembly;
+
+public class CalledClass
+{
+ public static void CalledMethod() { }
+}
+
+public class ClassWithAsyncMethod
+{
+ public async Task AsyncMethod()
+ {
+ await Task.Yield();
+ CalledClass.CalledMethod();
+ }
+}
+
+public class ClassWithIteratorMethod
+{
+ public IEnumerable IteratorMethod()
+ {
+ CalledClass.CalledMethod();
+ yield return 0;
+ }
+}