diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000000..41fb5ab87b
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,18 @@
+version: 2
+updates:
+ - package-ecosystem: "nuget"
+ directory: "/"
+ open-pull-requests-limit: 20
+ schedule:
+ interval: "daily"
+ groups:
+ test-dependencies:
+ patterns:
+ - NUnit*
+ - "*Test*"
+ modularpipelines-dependencies:
+ patterns:
+ - "*ModularPipeline*"
+ ignore:
+ - dependency-name: "Microsoft.Extensions.*"
+ update-types: ["version-update:semver-major"]
diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
new file mode 100644
index 0000000000..f2eecf3162
--- /dev/null
+++ b/.github/workflows/dotnet.yml
@@ -0,0 +1,39 @@
+# This workflow will build a .NET project
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
+
+name: .NET
+
+on:
+ push:
+ branches: ["main"]
+ pull_request:
+ branches: ["main"]
+ workflow_dispatch:
+ inputs:
+ publish-packages:
+ description: Publish packages?
+ type: boolean
+ required: true
+
+jobs:
+ modularpipeline:
+ environment: ${{ github.ref == 'refs/heads/main' && 'Production' || 'Pull Requests' }}
+ runs-on: windows-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: 8.0.x
+ - name: Run Pipeline
+ run: dotnet run -c Release
+ working-directory: "TUnit.Pipeline"
+ env:
+ DOTNET_ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'Production' || 'Development' }}
+ NuGet__ApiKey: ${{ secrets.NUGET__APIKEY }}
+ PULL_REQUEST_BRANCH: ${{ github.event.pull_request.head.ref }}
+ PUBLISH_PACKAGES: ${{ github.event.inputs.publish-packages }}
diff --git a/.idea/.idea.TUnit/.idea/.gitignore b/.idea/.idea.TUnit/.idea/.gitignore
new file mode 100644
index 0000000000..0be7a56a1e
--- /dev/null
+++ b/.idea/.idea.TUnit/.idea/.gitignore
@@ -0,0 +1,13 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Rider ignored files
+/projectSettingsUpdater.xml
+/modules.xml
+/contentModel.xml
+/.idea.TUnit.iml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/.idea.TUnit/.idea/encodings.xml b/.idea/.idea.TUnit/.idea/encodings.xml
new file mode 100644
index 0000000000..df87cf951f
--- /dev/null
+++ b/.idea/.idea.TUnit/.idea/encodings.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.TUnit/.idea/indexLayout.xml b/.idea/.idea.TUnit/.idea/indexLayout.xml
new file mode 100644
index 0000000000..7b08163ceb
--- /dev/null
+++ b/.idea/.idea.TUnit/.idea/indexLayout.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.TUnit/.idea/vcs.xml b/.idea/.idea.TUnit/.idea/vcs.xml
new file mode 100644
index 0000000000..35eb1ddfbb
--- /dev/null
+++ b/.idea/.idea.TUnit/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000000..85f8db039f
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,16 @@
+
+
+
+ <_Parameter1>TUnit.Core
+
+
+ <_Parameter1>TUnit.Engine
+
+
+ <_Parameter1>TUnit
+
+
+ <_Parameter1>TUnit.TestAdapter
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index a32891afa2..4e6bda8c71 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,62 @@
-# TUnit
\ No newline at end of file
+# TUnit
+T(est)Unit!
+
+```csharp
+ [Test]
+ public async Task Test1()
+ {
+ var value = "Hello world!";
+
+ await Assert.That(value)
+ .Is.Not.Null
+ .And.Is.EqualTo("hello world!", StringComparison.InvariantCultureIgnoreCase)
+ .And.Has.Count().EqualTo(12)
+ .And.Does.StartWith("H");
+ }
+```
+
+## Motivations
+There are only three main testing frameworks in the .NET world - xUnit, NUnit and MSTest.
+More frameworks means more options, and more options motivates more features or improvements.
+
+These testing frameworks are amazing, but I've had some issues with them. You might not have had any of these, but these are my experiences:
+
+### xUnit
+There is no way to tap into information about a test in a generic way.
+For example, I've had some Playwright tests run before, and I want them to save a screenshot or video ONLY when the test fails.
+If the test passes, I don't have anything to investigate, and it'll use up unnecessary storage, and it'll probably slow my test suite down if I had hundreds or thousands of tests all trying to save screenshots.
+
+However, if I'm in a Dispose method which is called when the test ends, then there's no way for me to know if my test succeeded or failed. I'd have to do some really clunky workaround involving try catch and setting a boolean or exception to a class field and checking that. And to do that for every test was just not ideal.
+
+#### Assertions
+I have stumbled across assertions so many times where the arguments are the wrong way round.
+This can result in really confusing error messages.
+```csharp
+var one = 2;
+Assert.Equal(1, one)
+Assert.Equal(one, 1)
+```
+
+### NUnit
+
+#### Assertions
+I absolutely love the newer assertion syntax in NUnit. The `Assert.That(something, Is.Something)`. I think it's really clear to read, it's clear what is being asserted, and it's clear what you're trying to achieve.
+
+However, there is a lack of type checking on assertions. (Yes, there are analyzer packages to help with this, but this still isn't strict type checking.)
+
+`Assert.That("1", Throws.Exception);`
+
+This assertion makes no sense, because we're passing in a string. This can never throw an exception because it isn't a delegate that can be executed. But it's still perfectly valid code that will compile.
+
+As does this:
+`Assert.That(1, Does.Contain("Foo!"));`
+
+With TUnit assertions, I wanted to make these impossible to compile. So type constraints are built into the assertions themselves. There should be no way for a non-delegate to be able to do a `Throws` assertion, or for an `int` assertion to check for `string` conditions.
+
+So in TUnit, this will compile:
+
+`await Assert.That(() => 1).Throws.Nothing;`
+
+This won't:
+
+`await Assert.That(1).Throws.Nothing;`
\ No newline at end of file
diff --git a/TUnit.Analyzers/TUnit.Analyzers.Sample/Examples.cs b/TUnit.Analyzers/TUnit.Analyzers.Sample/Examples.cs
new file mode 100644
index 0000000000..5a79910110
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers.Sample/Examples.cs
@@ -0,0 +1,24 @@
+// ReSharper disable UnusedType.Global
+// ReSharper disable UnusedMember.Global
+
+using System.Threading.Tasks;
+using TUnit.Assertions;
+
+namespace TUnit.Analyzers.Sample;
+
+// If you don't see warnings, build the Analyzers Project.
+
+public class Examples
+{
+ public class CommonClass // Try to apply quick fix using the IDE.
+ {
+ }
+
+ public async Task ToStars()
+ {
+ await Assert.That("1").Is.EqualTo("2");
+ var spaceship = new Spaceship();
+ spaceship.SetSpeed(300000000); // Invalid value, it should be highlighted.
+ spaceship.SetSpeed(42);
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Analyzers/TUnit.Analyzers.Sample/Spaceship.cs b/TUnit.Analyzers/TUnit.Analyzers.Sample/Spaceship.cs
new file mode 100644
index 0000000000..432f4431ba
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers.Sample/Spaceship.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace TUnit.Analyzers.Sample;
+
+public class Spaceship
+{
+ public void SetSpeed(long speed)
+ {
+ if (speed > 299_792_458)
+ throw new ArgumentOutOfRangeException(nameof(speed));
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Analyzers/TUnit.Analyzers.Sample/TUnit.Analyzers.Sample.csproj b/TUnit.Analyzers/TUnit.Analyzers.Sample/TUnit.Analyzers.Sample.csproj
new file mode 100644
index 0000000000..0937190a22
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers.Sample/TUnit.Analyzers.Sample.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net7.0
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/TUnit.Analyzers/TUnit.Analyzers.Tests/AwaitAssertionAnalyzerTests.cs b/TUnit.Analyzers/TUnit.Analyzers.Tests/AwaitAssertionAnalyzerTests.cs
new file mode 100644
index 0000000000..4f5efe475f
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers.Tests/AwaitAssertionAnalyzerTests.cs
@@ -0,0 +1,33 @@
+using System.Threading.Tasks;
+using NUnit.Framework;
+using Verifier = TUnit.Analyzers.Tests.Verifiers.CSharpAnalyzerVerifier;
+
+namespace TUnit.Analyzers.Tests;
+
+public class AwaitAssertionAnalyzerTests
+{
+ [Test]
+ public async Task ClassWithMyCompanyTitle_AlertDiagnostic()
+ {
+ const string text = """
+ using System.Threading.Tasks;
+ using TUnit.Assertions;
+ using TUnit.Core;
+
+ public class MyClass
+ {
+
+ public async Task MyTest()
+ {
+ var one = 1;
+ {|#0:Assert.That(one).Is.EqualTo(1);|}
+ }
+
+ }
+ """;
+
+ var expected = Verifier.Diagnostic().WithLocation(0);
+
+ await Verifier.VerifyAnalyzerAsync(text, expected).ConfigureAwait(false);
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Analyzers/TUnit.Analyzers.Tests/Net.cs b/TUnit.Analyzers/TUnit.Analyzers.Tests/Net.cs
new file mode 100644
index 0000000000..fc95ec3cc2
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers.Tests/Net.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using Microsoft.CodeAnalysis.Testing;
+
+namespace TUnit.Analyzers.Tests;
+
+internal static class Net
+{
+ private static readonly Lazy LazyNet60 = new(() =>
+ new ReferenceAssemblies(
+ "net6.0",
+ new PackageIdentity(
+ "Microsoft.NETCore.App.Ref",
+ "6.0.21"),
+ Path.Combine("ref", "net6.0"))
+ .AddPackages(ImmutableArray.Create(new PackageIdentity("Microsoft.Extensions.Logging", "6.0.0")))
+ );
+
+ public static ReferenceAssemblies Net60 => LazyNet60.Value;
+
+ private static readonly Lazy LazyNet70 = new(() =>
+ new ReferenceAssemblies(
+ "net7.0",
+ new PackageIdentity(
+ "Microsoft.NETCore.App.Ref",
+ "7.0.15"),
+ Path.Combine("ref", "net7.0"))
+ .AddPackages(ImmutableArray.Create(new PackageIdentity("Microsoft.Extensions.Logging", "7.0.0")))
+ );
+
+ private static readonly Lazy LazyNet80 = new(() =>
+ new ReferenceAssemblies(
+ "net8.0",
+ new PackageIdentity(
+ "Microsoft.NETCore.App.Ref",
+ "8.0.1"),
+ Path.Combine("ref", "net8.0"))
+ .AddPackages(ImmutableArray.Create(new PackageIdentity("Microsoft.Extensions.Logging", "8.0.0")))
+ );
+
+ public static ReferenceAssemblies Net70 => LazyNet70.Value;
+ public static ReferenceAssemblies Net80 => LazyNet80.Value;
+}
\ No newline at end of file
diff --git a/TUnit.Analyzers/TUnit.Analyzers.Tests/SampleCodeFixProviderTests.cs b/TUnit.Analyzers/TUnit.Analyzers.Tests/SampleCodeFixProviderTests.cs
new file mode 100644
index 0000000000..1eebd3b369
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers.Tests/SampleCodeFixProviderTests.cs
@@ -0,0 +1,31 @@
+// using System.Threading.Tasks;
+// using Xunit;
+// using Verifier =
+// Microsoft.CodeAnalysis.CSharp.Testing.XUnit.CodeFixVerifier;
+//
+// namespace TUnit.Analyzers.Tests;
+//
+// public class SampleCodeFixProviderTests
+// {
+// [Fact]
+// public async Task ClassWithMyCompanyTitle_ReplaceWithCommonKeyword()
+// {
+// const string text = @"
+// public class MyCompanyClass
+// {
+// }
+// ";
+//
+// const string newText = @"
+// public class CommonClass
+// {
+// }
+// ";
+//
+// var expected = Verifier.Diagnostic()
+// .WithLocation(2, 14)
+// .WithArguments("MyCompanyClass");
+// await Verifier.VerifyCodeFixAsync(text, expected, newText).ConfigureAwait(false);
+// }
+// }
\ No newline at end of file
diff --git a/TUnit.Analyzers/TUnit.Analyzers.Tests/TUnit.Analyzers.Tests.csproj b/TUnit.Analyzers/TUnit.Analyzers.Tests/TUnit.Analyzers.Tests.csproj
new file mode 100644
index 0000000000..87da5d872f
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers.Tests/TUnit.Analyzers.Tests.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net7.0
+ enable
+
+ false
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
diff --git a/TUnit.Analyzers/TUnit.Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier.cs b/TUnit.Analyzers/TUnit.Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier.cs
new file mode 100644
index 0000000000..8c52e6fc73
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier.cs
@@ -0,0 +1,47 @@
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Testing;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Testing.Verifiers;
+
+namespace TUnit.Analyzers.Tests.Verifiers;
+
+public static partial class CSharpAnalyzerVerifier
+ where TAnalyzer : DiagnosticAnalyzer, new()
+{
+ public class Test : CSharpAnalyzerTest
+ {
+ public Test()
+ {
+ SolutionTransforms.Add((solution, projectId) =>
+ {
+ var project = solution.GetProject(projectId);
+
+ if (project is null)
+ {
+ return solution;
+ }
+
+ var compilationOptions = project.CompilationOptions;
+
+ if (compilationOptions is null)
+ {
+ return solution;
+ }
+
+ var parseOptions = project.ParseOptions as CSharpParseOptions;
+
+ if (parseOptions is null)
+ {
+ return solution;
+ }
+
+ compilationOptions = compilationOptions.WithSpecificDiagnosticOptions(compilationOptions.SpecificDiagnosticOptions.SetItems(CSharpVerifierHelper.NullableWarnings));
+
+ solution = solution.WithProjectCompilationOptions(projectId, compilationOptions)
+ .WithProjectParseOptions(projectId, parseOptions.WithLanguageVersion(LanguageVersion.Preview));
+
+ return solution;
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Analyzers/TUnit.Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier`1.cs b/TUnit.Analyzers/TUnit.Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier`1.cs
new file mode 100644
index 0000000000..51fa0c503b
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier`1.cs
@@ -0,0 +1,48 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Testing;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Testing;
+using Microsoft.CodeAnalysis.Testing.Verifiers;
+using TUnit.Assertions;
+using TUnit.Core;
+
+namespace TUnit.Analyzers.Tests.Verifiers;
+
+public static partial class CSharpAnalyzerVerifier
+ where TAnalyzer : DiagnosticAnalyzer, new()
+{
+ ///
+ public static DiagnosticResult Diagnostic()
+ => CSharpAnalyzerVerifier.Diagnostic();
+
+ ///
+ public static DiagnosticResult Diagnostic(string diagnosticId)
+ => CSharpAnalyzerVerifier.Diagnostic(diagnosticId);
+
+ ///
+ public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor)
+ => CSharpAnalyzerVerifier.Diagnostic(descriptor);
+
+ ///
+ public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected)
+ {
+ var test = new Test
+ {
+ TestCode = source,
+ ReferenceAssemblies = Net.Net80,
+ TestState =
+ {
+ AdditionalReferences =
+ {
+ typeof(TUnitAttribute).Assembly.Location,
+ typeof(AssertionBuilder<>).Assembly.Location,
+ },
+ },
+ };
+
+ test.ExpectedDiagnostics.AddRange(expected);
+ await test.RunAsync(CancellationToken.None);
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Analyzers/TUnit.Analyzers.Tests/Verifiers/CSharpCodeFixVerifier.cs b/TUnit.Analyzers/TUnit.Analyzers.Tests/Verifiers/CSharpCodeFixVerifier.cs
new file mode 100644
index 0000000000..4074d1ac9e
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers.Tests/Verifiers/CSharpCodeFixVerifier.cs
@@ -0,0 +1,49 @@
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Testing;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Testing.Verifiers;
+
+namespace TUnit.Analyzers.Tests.Verifiers;
+
+public static partial class CSharpCodeFixVerifier
+ where TAnalyzer : DiagnosticAnalyzer, new()
+ where TCodeFix : CodeFixProvider, new()
+{
+ public class Test : CSharpCodeFixTest
+ {
+ public Test()
+ {
+ SolutionTransforms.Add((solution, projectId) =>
+ {
+ var project = solution.GetProject(projectId);
+
+ if (project is null)
+ {
+ return solution;
+ }
+
+ var compilationOptions = project.CompilationOptions;
+
+ if (compilationOptions is null)
+ {
+ return solution;
+ }
+
+ var parseOptions = project.ParseOptions as CSharpParseOptions;
+
+ if (parseOptions is null)
+ {
+ return solution;
+ }
+
+ compilationOptions = compilationOptions.WithSpecificDiagnosticOptions(compilationOptions.SpecificDiagnosticOptions.SetItems(CSharpVerifierHelper.NullableWarnings));
+
+ solution = solution.WithProjectCompilationOptions(projectId, compilationOptions)
+ .WithProjectParseOptions(projectId, parseOptions.WithLanguageVersion(LanguageVersion.Preview));
+
+ return solution;
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Analyzers/TUnit.Analyzers.Tests/Verifiers/CSharpCodeFixVerifier`2.cs b/TUnit.Analyzers/TUnit.Analyzers.Tests/Verifiers/CSharpCodeFixVerifier`2.cs
new file mode 100644
index 0000000000..2000ecec69
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers.Tests/Verifiers/CSharpCodeFixVerifier`2.cs
@@ -0,0 +1,80 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp.Testing;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Testing;
+using Microsoft.CodeAnalysis.Testing.Verifiers;
+using TUnit.Assertions;
+using TUnit.Core;
+
+namespace TUnit.Analyzers.Tests.Verifiers;
+
+public static partial class CSharpCodeFixVerifier
+ where TAnalyzer : DiagnosticAnalyzer, new()
+ where TCodeFix : CodeFixProvider, new()
+{
+ ///
+ public static DiagnosticResult Diagnostic()
+ => CSharpCodeFixVerifier.Diagnostic();
+
+ ///
+ public static DiagnosticResult Diagnostic(string diagnosticId)
+ => CSharpCodeFixVerifier.Diagnostic(diagnosticId);
+
+ ///
+ public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor)
+ => CSharpCodeFixVerifier.Diagnostic(descriptor);
+
+ ///
+ public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected)
+ {
+ var test = new Test
+ {
+ TestCode = source,
+ CodeActionValidationMode = CodeActionValidationMode.SemanticStructure,
+ ReferenceAssemblies = Net.Net80,
+ TestState =
+ {
+ AdditionalReferences =
+ {
+ typeof(TUnitAttribute).Assembly.Location,
+ typeof(AssertionBuilder<>).Assembly.Location,
+ },
+ },
+ };
+
+ test.ExpectedDiagnostics.AddRange(expected);
+ await test.RunAsync(CancellationToken.None);
+ }
+
+ ///
+ public static async Task VerifyCodeFixAsync(string source, string fixedSource)
+ => await VerifyCodeFixAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource);
+
+ ///
+ public static async Task VerifyCodeFixAsync(string source, DiagnosticResult expected, string fixedSource)
+ => await VerifyCodeFixAsync(source, new[] { expected }, fixedSource);
+
+ ///
+ public static async Task VerifyCodeFixAsync(string source, DiagnosticResult[] expected, string fixedSource)
+ {
+ var test = new Test
+ {
+ TestCode = source,
+ FixedCode = fixedSource,
+ ReferenceAssemblies = Net.Net80,
+ TestState =
+ {
+ AdditionalReferences =
+ {
+ typeof(TUnitAttribute).Assembly.Location,
+ typeof(AssertionBuilder<>).Assembly.Location,
+ }, },
+ };
+
+ test.ExpectedDiagnostics.AddRange(expected);
+ await test.RunAsync(CancellationToken.None);
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Analyzers/TUnit.Analyzers.Tests/Verifiers/CSharpCodeRefactoringVerifier.cs b/TUnit.Analyzers/TUnit.Analyzers.Tests/Verifiers/CSharpCodeRefactoringVerifier.cs
new file mode 100644
index 0000000000..7fa594b0af
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers.Tests/Verifiers/CSharpCodeRefactoringVerifier.cs
@@ -0,0 +1,47 @@
+using Microsoft.CodeAnalysis.CodeRefactorings;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Testing;
+using Microsoft.CodeAnalysis.Testing.Verifiers;
+
+namespace TUnit.Analyzers.Tests.Verifiers;
+
+public static partial class CSharpCodeRefactoringVerifier
+ where TCodeRefactoring : CodeRefactoringProvider, new()
+{
+ public class Test : CSharpCodeRefactoringTest
+ {
+ public Test()
+ {
+ SolutionTransforms.Add((solution, projectId) =>
+ {
+ var project = solution.GetProject(projectId);
+
+ if (project is null)
+ {
+ return solution;
+ }
+
+ var compilationOptions = project.CompilationOptions;
+
+ if (compilationOptions is null)
+ {
+ return solution;
+ }
+
+ var parseOptions = project.ParseOptions as CSharpParseOptions;
+
+ if (parseOptions is null)
+ {
+ return solution;
+ }
+
+ compilationOptions = compilationOptions.WithSpecificDiagnosticOptions(compilationOptions.SpecificDiagnosticOptions.SetItems(CSharpVerifierHelper.NullableWarnings));
+
+ solution = solution.WithProjectCompilationOptions(projectId, compilationOptions)
+ .WithProjectParseOptions(projectId, parseOptions.WithLanguageVersion(LanguageVersion.Preview));
+
+ return solution;
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Analyzers/TUnit.Analyzers.Tests/Verifiers/CSharpCodeRefactoringVerifier`1.cs b/TUnit.Analyzers/TUnit.Analyzers.Tests/Verifiers/CSharpCodeRefactoringVerifier`1.cs
new file mode 100644
index 0000000000..1bbdb42268
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers.Tests/Verifiers/CSharpCodeRefactoringVerifier`1.cs
@@ -0,0 +1,35 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.CodeRefactorings;
+using Microsoft.CodeAnalysis.Testing;
+
+namespace TUnit.Analyzers.Tests.Verifiers;
+
+public static partial class CSharpCodeRefactoringVerifier
+ where TCodeRefactoring : CodeRefactoringProvider, new()
+{
+ ///
+ public static async Task VerifyRefactoringAsync(string source, string fixedSource)
+ {
+ await VerifyRefactoringAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource);
+ }
+
+ ///
+ public static async Task VerifyRefactoringAsync(string source, DiagnosticResult expected, string fixedSource)
+ {
+ await VerifyRefactoringAsync(source, new[] { expected }, fixedSource);
+ }
+
+ ///
+ public static async Task VerifyRefactoringAsync(string source, DiagnosticResult[] expected, string fixedSource)
+ {
+ var test = new Test
+ {
+ TestCode = source,
+ FixedCode = fixedSource,
+ };
+
+ test.ExpectedDiagnostics.AddRange(expected);
+ await test.RunAsync(CancellationToken.None);
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Analyzers/TUnit.Analyzers.Tests/Verifiers/CSharpVerifierHelper.cs b/TUnit.Analyzers/TUnit.Analyzers.Tests/Verifiers/CSharpVerifierHelper.cs
new file mode 100644
index 0000000000..79517165e5
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers.Tests/Verifiers/CSharpVerifierHelper.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace TUnit.Analyzers.Tests.Verifiers;
+
+internal static class CSharpVerifierHelper
+{
+ ///
+ /// By default, the compiler reports diagnostics for nullable reference types at
+ /// , and the analyzer test framework defaults to only validating
+ /// diagnostics at . This map contains all compiler diagnostic IDs
+ /// related to nullability mapped to , which is then used to enable all
+ /// of these warnings for default validation during analyzer and code fix tests.
+ ///
+ internal static ImmutableDictionary NullableWarnings { get; } = GetNullableWarningsFromCompiler();
+
+ private static ImmutableDictionary GetNullableWarningsFromCompiler()
+ {
+ string[] args = { "/warnaserror:nullable", "-p:LangVersion=preview" };
+ var commandLineArguments = CSharpCommandLineParser.Default.Parse(args, baseDirectory: Environment.CurrentDirectory, sdkDirectory: Environment.CurrentDirectory);
+ var nullableWarnings = commandLineArguments.CompilationOptions.SpecificDiagnosticOptions;
+
+ // Workaround for https://github.com/dotnet/roslyn/issues/41610
+ nullableWarnings = nullableWarnings
+ .SetItem("CS8632", ReportDiagnostic.Error)
+ .SetItem("CS8669", ReportDiagnostic.Error)
+ .SetItem("CS8652", ReportDiagnostic.Suppress);
+
+ return nullableWarnings;
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Analyzers/TUnit.Analyzers/AnalyzerReleases.Shipped.md b/TUnit.Analyzers/TUnit.Analyzers/AnalyzerReleases.Shipped.md
new file mode 100644
index 0000000000..b49126e4dc
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers/AnalyzerReleases.Shipped.md
@@ -0,0 +1,8 @@
+## Release 1.0
+
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|------------------------------------------------
+TUnit0001 | Usage | Warning | Don't mix 'Or' & 'And' operators in assertions.
+TUnit0002 | Usage | Error | Assert statements must be awaited.
diff --git a/TUnit.Analyzers/TUnit.Analyzers/AnalyzerReleases.Unshipped.md b/TUnit.Analyzers/TUnit.Analyzers/AnalyzerReleases.Unshipped.md
new file mode 100644
index 0000000000..062067f471
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,4 @@
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|-------
\ No newline at end of file
diff --git a/TUnit.Analyzers/TUnit.Analyzers/AwaitAssertionAnalyzer.cs b/TUnit.Analyzers/TUnit.Analyzers/AwaitAssertionAnalyzer.cs
new file mode 100644
index 0000000000..78cc8332c3
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers/AwaitAssertionAnalyzer.cs
@@ -0,0 +1,109 @@
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+using TUnit.Analyzers.Extensions;
+
+namespace TUnit.Analyzers;
+
+///
+/// A sample analyzer that reports the company name being used in class declarations.
+/// Traverses through the Syntax Tree and checks the name (identifier) of each class node.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class AwaitAssertionAnalyzer : DiagnosticAnalyzer
+{
+ // Preferred format of DiagnosticId is Your Prefix + Number, e.g. CA1234.
+ public const string DiagnosticId = "TUnit0002";
+
+ // Feel free to use raw strings if you don't need localization.
+ private static readonly LocalizableString Title = new LocalizableResourceString(DiagnosticId + "Title",
+ Resources.ResourceManager, typeof(Resources));
+
+ // The message that will be displayed to the user.
+ private static readonly LocalizableString MessageFormat =
+ new LocalizableResourceString(DiagnosticId + "MessageFormat", Resources.ResourceManager,
+ typeof(Resources));
+
+ private static readonly LocalizableString Description =
+ new LocalizableResourceString(DiagnosticId + "Description", Resources.ResourceManager,
+ typeof(Resources));
+
+ // The category of the diagnostic (Design, Naming etc.).
+ private const string Category = "Usage";
+
+ private static readonly DiagnosticDescriptor Rule = new(DiagnosticId, Title, MessageFormat, Category,
+ DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description);
+
+ // Keep in mind: you have to list your rules here.
+ public override ImmutableArray SupportedDiagnostics { get; } =
+ ImmutableArray.Create(Rule);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ // You must call this method to avoid analyzing generated code.
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+
+ // You must call this method to enable the Concurrent Execution.
+ context.EnableConcurrentExecution();
+
+ // Subscribe to the Syntax Node with the appropriate 'SyntaxKind' (ClassDeclaration) action.
+ // To figure out which Syntax Nodes you should choose, consider installing the Roslyn syntax tree viewer plugin Rossynt: https://plugins.jetbrains.com/plugin/16902-rossynt/
+ context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.SimpleMemberAccessExpression);
+
+ // Check other 'context.Register...' methods that might be helpful for your purposes.
+ }
+
+ ///
+ /// Executed for each Syntax Node with 'SyntaxKind' is 'ClassDeclaration'.
+ ///
+ /// Operation context.
+ private void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
+ {
+ // The Roslyn architecture is based on inheritance.
+ // To get the required metadata, we should match the 'Node' object to the particular type: 'ClassDeclarationSyntax'.
+ if (context.Node is not MemberAccessExpressionSyntax memberAccessExpressionSyntax)
+ {
+ return;
+ }
+
+ if (memberAccessExpressionSyntax.ToString() != "Assert.That")
+ {
+ return;
+ }
+
+ var symbol = context.SemanticModel.GetSymbolInfo(memberAccessExpressionSyntax);
+
+ if (symbol.Symbol is not IMethodSymbol methodSymbol)
+ {
+ return;
+ }
+
+ if (methodSymbol.ToDisplayString(DisplayFormats.FullyQualifiedNonGeneric) !=
+ "global::TUnit.Assertions.Assert.That")
+ {
+ return;
+ }
+
+ var expressionStatementParent = memberAccessExpressionSyntax.GetAncestorSyntaxOfType();
+
+ if (expressionStatementParent is null)
+ {
+ return;
+ }
+
+ if (expressionStatementParent.ChildNodes().Any(x => x is AwaitExpressionSyntax))
+ {
+ return;
+ }
+
+ context.ReportDiagnostic(
+ Diagnostic.Create(
+ new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error,
+ true, Description),
+ expressionStatementParent.GetLocation())
+ );
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Analyzers/TUnit.Analyzers/AwaitAssertionCodeFixProvider.cs b/TUnit.Analyzers/TUnit.Analyzers/AwaitAssertionCodeFixProvider.cs
new file mode 100644
index 0000000000..121e2d3abc
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers/AwaitAssertionCodeFixProvider.cs
@@ -0,0 +1,74 @@
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Editing;
+
+namespace TUnit.Analyzers;
+
+///
+/// A sample code fix provider that renames classes with the company name in their definition.
+/// All code fixes must be linked to specific analyzers.
+///
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AwaitAssertionCodeFixProvider)), Shared]
+public class AwaitAssertionCodeFixProvider : CodeFixProvider
+{
+ // Specify the diagnostic IDs of analyzers that are expected to be linked.
+ public sealed override ImmutableArray FixableDiagnosticIds { get; } =
+ ImmutableArray.Create(AwaitAssertionAnalyzer.DiagnosticId);
+
+ // If you don't need the 'fix all' behaviour, return null.
+ public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
+
+ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ // We link only one diagnostic and assume there is only one diagnostic in the context.
+ var diagnostic = context.Diagnostics.Single();
+
+ // 'SourceSpan' of 'Location' is the highlighted area. We're going to use this area to find the 'SyntaxNode' to rename.
+ var diagnosticSpan = diagnostic.Location.SourceSpan;
+
+ // Get the root of Syntax Tree that contains the highlighted diagnostic.
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+
+ // Find SyntaxNode corresponding to the diagnostic.
+ var diagnosticNode = root?.FindNode(diagnosticSpan);
+
+ // To get the required metadata, we should match the Node to the specific type: 'ClassDeclarationSyntax'.
+ if (diagnosticNode is not ExpressionStatementSyntax expressionStatementSyntax)
+ return;
+
+ // Register a code action that will invoke the fix.
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: Resources.TUnit0002CodeFixTitle,
+ createChangedDocument: c => AwaitAssertionAsync(context.Document, expressionStatementSyntax, c),
+ equivalenceKey: nameof(Resources.TUnit0002CodeFixTitle)),
+ diagnostic);
+ }
+
+ ///
+ /// Executed on the quick fix action raised by the user.
+ ///
+ /// Affected source file.
+ /// Highlighted class declaration Syntax Node.
+ /// Any fix is cancellable by the user, so we should support the cancellation token.
+ /// Clone of the solution with updates: renamed class.
+ private async Task AwaitAssertionAsync(Document document,
+ ExpressionStatementSyntax expressionStatementSyntax, CancellationToken cancellationToken)
+ {
+ var editor = await DocumentEditor.CreateAsync(document, cancellationToken);
+
+ var awaitExpressionSyntax = SyntaxFactory.AwaitExpression(expressionStatementSyntax.Expression);
+
+ editor.ReplaceNode(expressionStatementSyntax.Expression, awaitExpressionSyntax);
+
+ return editor.GetChangedDocument();
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Analyzers/TUnit.Analyzers/DisplayFormats.cs b/TUnit.Analyzers/TUnit.Analyzers/DisplayFormats.cs
new file mode 100644
index 0000000000..abab77fa08
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers/DisplayFormats.cs
@@ -0,0 +1,19 @@
+using Microsoft.CodeAnalysis;
+
+namespace TUnit.Analyzers;
+
+public class DisplayFormats
+{
+ public static SymbolDisplayFormat FullyQualifiedNonGeneric => new(
+ SymbolDisplayGlobalNamespaceStyle.Included,
+ SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
+ SymbolDisplayGenericsOptions.None,
+ SymbolDisplayMemberOptions.IncludeContainingType,
+ SymbolDisplayDelegateStyle.NameAndSignature,
+ SymbolDisplayExtensionMethodStyle.Default,
+ SymbolDisplayParameterOptions.IncludeType,
+ SymbolDisplayPropertyStyle.NameOnly,
+ SymbolDisplayLocalOptions.IncludeType,
+ SymbolDisplayKindOptions.None
+ );
+}
\ No newline at end of file
diff --git a/TUnit.Analyzers/TUnit.Analyzers/Extensions/SyntaxExtensions.cs b/TUnit.Analyzers/TUnit.Analyzers/Extensions/SyntaxExtensions.cs
new file mode 100644
index 0000000000..e4caed1e33
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers/Extensions/SyntaxExtensions.cs
@@ -0,0 +1,19 @@
+using Microsoft.CodeAnalysis;
+
+namespace TUnit.Analyzers.Extensions;
+
+public static class SyntaxExtensions
+{
+ public static TOutput? GetAncestorSyntaxOfType(this SyntaxNode input)
+ where TOutput : SyntaxNode
+ {
+ var parent = input.Parent;
+
+ while (parent != null && parent is not TOutput)
+ {
+ parent = parent.Parent;
+ }
+
+ return parent as TOutput;
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Analyzers/TUnit.Analyzers/MixAndOrOperatorsAnalyzer.cs b/TUnit.Analyzers/TUnit.Analyzers/MixAndOrOperatorsAnalyzer.cs
new file mode 100644
index 0000000000..0a75fb6475
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers/MixAndOrOperatorsAnalyzer.cs
@@ -0,0 +1,94 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+using TUnit.Analyzers.Extensions;
+
+namespace TUnit.Analyzers;
+
+///
+/// A sample analyzer that reports the company name being used in class declarations.
+/// Traverses through the Syntax Tree and checks the name (identifier) of each class node.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class MixAndOrOperatorsAnalyzer : DiagnosticAnalyzer
+{
+ // Preferred format of DiagnosticId is Your Prefix + Number, e.g. CA1234.
+ public const string DiagnosticId = "TUnit0001";
+
+ // Feel free to use raw strings if you don't need localization.
+ private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.TUnit0001Title),
+ Resources.ResourceManager, typeof(Resources));
+
+ // The message that will be displayed to the user.
+ private static readonly LocalizableString MessageFormat =
+ new LocalizableResourceString(nameof(Resources.TUnit0001MessageFormat), Resources.ResourceManager,
+ typeof(Resources));
+
+ private static readonly LocalizableString Description =
+ new LocalizableResourceString(nameof(Resources.TUnit0001Description), Resources.ResourceManager,
+ typeof(Resources));
+
+ private const string Category = "Usage";
+
+ private static readonly DiagnosticDescriptor Rule = new(DiagnosticId, Title, MessageFormat, Category,
+ DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);
+
+ // Keep in mind: you have to list your rules here.
+ public override ImmutableArray SupportedDiagnostics { get; } =
+ ImmutableArray.Create(Rule);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ // You must call this method to avoid analyzing generated code.
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+
+ // You must call this method to enable the Concurrent Execution.
+ context.EnableConcurrentExecution();
+
+ // Subscribe to the Syntax Node with the appropriate 'SyntaxKind' (ClassDeclaration) action.
+ // To figure out which Syntax Nodes you should choose, consider installing the Roslyn syntax tree viewer plugin Rossynt: https://plugins.jetbrains.com/plugin/16902-rossynt/
+ context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.SimpleMemberAccessExpression);
+ }
+
+ private static void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
+ {
+ // The Roslyn architecture is based on inheritance.
+ // To get the required metadata, we should match the 'Node' object to the particular type: 'ClassDeclarationSyntax'.
+ if (context.Node is not MemberAccessExpressionSyntax memberAccessExpressionSyntax)
+ return;
+
+ if (memberAccessExpressionSyntax.Name.Identifier.Value is not "And" and not "Or")
+ {
+ return;
+ }
+
+ if (memberAccessExpressionSyntax.GetAncestorSyntaxOfType() is not { } invocationExpressionSyntax)
+ {
+ return;
+ }
+
+ var symbolInfo = context.SemanticModel.GetSymbolInfo(invocationExpressionSyntax);
+
+ var fullyQualifiedSymbolInformation = symbolInfo.Symbol?.ToDisplayString(DisplayFormats.FullyQualifiedNonGeneric)
+ ?? string.Empty;
+
+ if (!fullyQualifiedSymbolInformation.StartsWith("global::TUnit.Assertions"))
+ {
+ return;
+ }
+
+ var fullInvocationStatement = invocationExpressionSyntax.ToFullString();
+ if(fullInvocationStatement.Contains(".And.")
+ && fullInvocationStatement.Contains(".Or."))
+ {
+ context.ReportDiagnostic(
+ Diagnostic.Create(
+ new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning,
+ true, Description),
+ invocationExpressionSyntax.GetLocation())
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Analyzers/TUnit.Analyzers/Properties/launchSettings.json b/TUnit.Analyzers/TUnit.Analyzers/Properties/launchSettings.json
new file mode 100644
index 0000000000..5ff5a737e1
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers/Properties/launchSettings.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "DebugRoslynAnalyzers": {
+ "commandName": "DebugRoslynComponent",
+ "targetProject": "../TUnit.Analyzers.Sample/TUnit.Analyzers.Sample.csproj"
+ }
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Analyzers/TUnit.Analyzers/Readme.md b/TUnit.Analyzers/TUnit.Analyzers/Readme.md
new file mode 100644
index 0000000000..7053db9f47
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers/Readme.md
@@ -0,0 +1,29 @@
+# Roslyn Analyzers Sample
+
+A set of three sample projects that includes Roslyn analyzers with code fix providers. Enjoy this template to learn from and modify analyzers for your own needs.
+
+## Content
+### TUnit.Analyzers
+A .NET Standard project with implementations of sample analyzers and code fix providers.
+**You must build this project to see the results (warnings) in the IDE.**
+
+- [SampleSemanticAnalyzer.cs](SampleSemanticAnalyzer.cs): An analyzer that reports invalid values used for the `speed` parameter of the `SetSpeed` function.
+- [AwaitAssertionAnalyzer.cs](AwaitAssertionAnalyzer.cs): An analyzer that reports the company name used in class definitions.
+- [SampleCodeFixProvider.cs](SampleCodeFixProvider.cs): A code fix that renames classes with company name in their definition. The fix is linked to [AwaitAssertionAnalyzer.cs](AwaitAssertionAnalyzer.cs).
+
+### TUnit.Analyzers.Sample
+A project that references the sample analyzers. Note the parameters of `ProjectReference` in [TUnit.Analyzers.Sample.csproj](../TUnit.Analyzers.Sample/TUnit.Analyzers.Sample.csproj), they make sure that the project is referenced as a set of analyzers.
+
+### TUnit.Analyzers.Tests
+Unit tests for the sample analyzers and code fix provider. The easiest way to develop language-related features is to start with unit tests.
+
+## How To?
+### How to debug?
+- Use the [launchSettings.json](Properties/launchSettings.json) profile.
+- Debug tests.
+
+### How can I determine which syntax nodes I should expect?
+Consider installing the Roslyn syntax tree viewer plugin [Rossynt](https://plugins.jetbrains.com/plugin/16902-rossynt/).
+
+### Learn more about wiring analyzers
+The complete set of information is available at [roslyn github repo wiki](https://github.com/dotnet/roslyn/blob/main/docs/wiki/README.md).
\ No newline at end of file
diff --git a/TUnit.Analyzers/TUnit.Analyzers/Resources.Designer.cs b/TUnit.Analyzers/TUnit.Analyzers/Resources.Designer.cs
new file mode 100644
index 0000000000..e760496b65
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers/Resources.Designer.cs
@@ -0,0 +1,134 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace TUnit.Analyzers {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("TUnit.Analyzers.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Don't mix 'Or' & 'And' operators in assertions.
+ ///
+ internal static string TUnit0001CodeFixTitle {
+ get {
+ return ResourceManager.GetString("TUnit0001CodeFixTitle", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Don't mix 'Or' & 'And' operators in assertions..
+ ///
+ internal static string TUnit0001Description {
+ get {
+ return ResourceManager.GetString("TUnit0001Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Don't mix 'Or' & 'And' operators in assertions.
+ ///
+ internal static string TUnit0001MessageFormat {
+ get {
+ return ResourceManager.GetString("TUnit0001MessageFormat", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Don't mix 'Or' & 'And' operators in assertions.
+ ///
+ internal static string TUnit0001Title {
+ get {
+ return ResourceManager.GetString("TUnit0001Title", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Assert statements must be awaited.
+ ///
+ internal static string TUnit0002CodeFixTitle {
+ get {
+ return ResourceManager.GetString("TUnit0002CodeFixTitle", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Assert statements must be awaited..
+ ///
+ internal static string TUnit0002Description {
+ get {
+ return ResourceManager.GetString("TUnit0002Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Assert statements must be awaited.
+ ///
+ internal static string TUnit0002MessageFormat {
+ get {
+ return ResourceManager.GetString("TUnit0002MessageFormat", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Assert statements must be awaited.
+ ///
+ internal static string TUnit0002Title {
+ get {
+ return ResourceManager.GetString("TUnit0002Title", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/TUnit.Analyzers/TUnit.Analyzers/Resources.resx b/TUnit.Analyzers/TUnit.Analyzers/Resources.resx
new file mode 100644
index 0000000000..97b894095d
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers/Resources.resx
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Don't mix 'Or' & 'And' operators in assertions.An optional longer localizable description of the diagnostic.
+
+
+ Don't mix 'Or' & 'And' operators in assertionsThe format-able message the diagnostic displays.
+
+
+ Don't mix 'Or' & 'And' operators in assertionsThe title of the diagnostic.
+
+
+ Don't mix 'Or' & 'And' operators in assertionsThe title of the code fix.
+
+
+ Assert statements must be awaited.An optional longer localizable description of the diagnostic.
+
+
+ Assert statements must be awaitedThe format-able message the diagnostic displays.
+
+
+ Assert statements must be awaitedThe title of the diagnostic.
+
+
+ Assert statements must be awaitedThe title of the code fix.
+
+
\ No newline at end of file
diff --git a/TUnit.Analyzers/TUnit.Analyzers/TUnit.Analyzers.csproj b/TUnit.Analyzers/TUnit.Analyzers/TUnit.Analyzers.csproj
new file mode 100644
index 0000000000..7b71b51bbc
--- /dev/null
+++ b/TUnit.Analyzers/TUnit.Analyzers/TUnit.Analyzers.csproj
@@ -0,0 +1,40 @@
+
+
+
+ netstandard2.0
+ false
+ enable
+ latest
+
+ true
+ true
+
+ TUnit.Analyzers
+ TUnit.Analyzers
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
+
+
+ True
+ True
+ Resources.resx
+
+
+
+
diff --git a/TUnit.Assertions.UnitTests/GlobalUsings.cs b/TUnit.Assertions.UnitTests/GlobalUsings.cs
new file mode 100644
index 0000000000..5e3c1307f3
--- /dev/null
+++ b/TUnit.Assertions.UnitTests/GlobalUsings.cs
@@ -0,0 +1,2 @@
+global using NUnit.Framework;
+global using Assert = TUnit.Assertions.Assert;
\ No newline at end of file
diff --git a/TUnit.Assertions.UnitTests/TUnit.Assertions.UnitTests.csproj b/TUnit.Assertions.UnitTests/TUnit.Assertions.UnitTests.csproj
new file mode 100644
index 0000000000..a4962c80df
--- /dev/null
+++ b/TUnit.Assertions.UnitTests/TUnit.Assertions.UnitTests.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+ false
+ true
+ latest
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
diff --git a/TUnit.Assertions.UnitTests/ZeroAssertionTests.cs b/TUnit.Assertions.UnitTests/ZeroAssertionTests.cs
new file mode 100644
index 0000000000..8d16eb5c28
--- /dev/null
+++ b/TUnit.Assertions.UnitTests/ZeroAssertionTests.cs
@@ -0,0 +1,47 @@
+namespace TUnit.Assertions.UnitTests;
+
+
+public class ZeroAssertionTests
+{
+ [Test]
+ public void Int()
+ {
+ int zero = 0;
+ Assert.That(zero);
+ }
+
+ [Test]
+ public void Long()
+ {
+ long zero = 0;
+ Assert.That(zero);
+ }
+
+ [Test]
+ public void Short()
+ {
+ short zero = 0;
+ Assert.That(zero);
+ }
+
+ [Test]
+ public void Int_Bad()
+ {
+ int zero = 1;
+ Assert.That(zero);
+ }
+
+ [Test]
+ public void Long_Bad()
+ {
+ long zero = 1;
+ Assert.That(zero);
+ }
+
+ [Test]
+ public void Short_Bad()
+ {
+ short zero = 1;
+ Assert.That(zero);
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/Assert.cs b/TUnit.Assertions/Assert.cs
new file mode 100644
index 0000000000..5a30db7d41
--- /dev/null
+++ b/TUnit.Assertions/Assert.cs
@@ -0,0 +1,29 @@
+namespace TUnit.Assertions;
+
+public static class Assert
+{
+ public static ValueAssertionBuilder That(TActual value)
+ {
+ return new ValueAssertionBuilder(value);
+ }
+
+ public static DelegateAssertionBuilder That(Action value)
+ {
+ return new DelegateAssertionBuilder(value);
+ }
+
+ public static DelegateAssertionBuilder That(Func value)
+ {
+ return new DelegateAssertionBuilder(value);
+ }
+
+ public static AsyncDelegateAssertionBuilder That(Func value)
+ {
+ return new AsyncDelegateAssertionBuilder(value);
+ }
+
+ public static AsyncDelegateAssertionBuilder That(Func> value)
+ {
+ return new AsyncDelegateAssertionBuilder(value!);
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/AssertCondition.cs b/TUnit.Assertions/AssertConditions/AssertCondition.cs
new file mode 100644
index 0000000000..306918ce6b
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/AssertCondition.cs
@@ -0,0 +1,15 @@
+using TUnit.Assertions.AssertConditions.Operators;
+
+namespace TUnit.Assertions.AssertConditions;
+
+public abstract class AssertCondition : BaseAssertCondition
+ where TAnd : And, IAnd
+ where TOr : Or, IOr
+{
+ internal TExpected? ExpectedValue { get; }
+
+ internal AssertCondition(AssertionBuilder assertionBuilder, TExpected? expected) : base(assertionBuilder)
+ {
+ ExpectedValue = expected;
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/BaseAssertCondition.cs b/TUnit.Assertions/AssertConditions/BaseAssertCondition.cs
new file mode 100644
index 0000000000..91db5f4569
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/BaseAssertCondition.cs
@@ -0,0 +1,77 @@
+using System.Runtime.CompilerServices;
+using TUnit.Assertions.AssertConditions.Operators;
+using TUnit.Assertions.Exceptions;
+
+namespace TUnit.Assertions.AssertConditions;
+
+public abstract class BaseAssertCondition
+ where TAnd : And, IAnd
+ where TOr : Or, IOr
+{
+ protected internal AssertionBuilder AssertionBuilder { get; }
+
+ internal BaseAssertCondition(AssertionBuilder assertionBuilder)
+ {
+ AssertionBuilder = assertionBuilder;
+
+ And = TAnd.Create(this);
+ Or = TOr.Create(this);
+ }
+
+ public TaskAwaiter GetAwaiter()
+ {
+ return AssertAsync().GetAwaiter();
+ }
+
+ private async Task AssertAsync()
+ {
+ var assertionData = await AssertionBuilder.GetAssertionData();
+
+ AssertAndThrow(assertionData.Result, assertionData.Exception);
+ }
+
+ protected TActual? ActualValue { get; private set; }
+ protected Exception? Exception { get; private set; }
+
+
+ protected internal virtual string Message => MessageFactory?.Invoke(ActualValue, Exception) ?? DefaultMessage;
+
+ private Func? MessageFactory { get; set; }
+
+ public BaseAssertCondition WithMessage(Func messageFactory)
+ {
+ MessageFactory = messageFactory;
+ return this;
+ }
+
+ protected abstract string DefaultMessage { get; }
+
+ private void AssertAndThrow(TActual? actual, Exception? exception)
+ {
+ if (!Assert(actual, exception))
+ {
+ throw new AssertionException(Message);
+ }
+ }
+
+ internal bool Assert(TActual? actualValue, Exception? exception)
+ {
+ ActualValue = actualValue;
+ Exception = exception;
+ return IsInverted ? !Passes(actualValue, exception) : Passes(actualValue, exception);
+ }
+
+ protected internal abstract bool Passes(TActual? actualValue, Exception? exception);
+
+ public TAnd And { get; }
+ public TOr Or { get; }
+
+ internal BaseAssertCondition Invert(Func messageFactory)
+ {
+ WithMessage(messageFactory);
+ IsInverted = true;
+ return this;
+ }
+
+ protected bool IsInverted { get; set; }
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/Collections/EnumerableContainsAssertCondition.cs b/TUnit.Assertions/AssertConditions/Collections/EnumerableContainsAssertCondition.cs
new file mode 100644
index 0000000000..e49a6d28df
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/Collections/EnumerableContainsAssertCondition.cs
@@ -0,0 +1,25 @@
+using TUnit.Assertions.AssertConditions.Operators;
+
+namespace TUnit.Assertions.AssertConditions.Collections;
+
+public class EnumerableContainsAssertCondition : AssertCondition
+ where TActual : IEnumerable
+ where TAnd : And, IAnd
+ where TOr : Or, IOr
+{
+ public EnumerableContainsAssertCondition(AssertionBuilder assertionBuilder, TInner expected) : base(assertionBuilder, expected)
+ {
+ }
+
+ protected override string DefaultMessage => $"{ExpectedValue} was not found in the collection";
+
+ protected internal override bool Passes(TActual? actualValue, Exception? exception)
+ {
+ if (actualValue is null)
+ {
+ return false;
+ }
+
+ return actualValue.Contains(ExpectedValue);
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/Collections/EnumerableCountEqualToAssertCondition.cs b/TUnit.Assertions/AssertConditions/Collections/EnumerableCountEqualToAssertCondition.cs
new file mode 100644
index 0000000000..834de5d168
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/Collections/EnumerableCountEqualToAssertCondition.cs
@@ -0,0 +1,38 @@
+using System.Collections;
+using TUnit.Assertions.AssertConditions.Operators;
+
+namespace TUnit.Assertions.AssertConditions.Collections;
+
+public class EnumerableCountEqualToAssertCondition : AssertCondition
+ where TActual : IEnumerable
+ where TAnd : And, IAnd
+ where TOr : Or, IOr
+{
+ public EnumerableCountEqualToAssertCondition(AssertionBuilder assertionBuilder, int expected) : base(assertionBuilder, expected)
+ {
+ }
+
+ protected override string DefaultMessage => $"Length is {GetCount(ActualValue)} instead of {ExpectedValue}";
+
+ protected internal override bool Passes(TActual? actualValue, Exception? exception)
+ {
+ return GetCount(actualValue) == ExpectedValue;
+ }
+
+ private int GetCount(TActual? actualValue)
+ {
+ ArgumentNullException.ThrowIfNull(actualValue);
+
+ if (actualValue is ICollection collection)
+ {
+ return collection.Count;
+ }
+
+ if (actualValue is TInner[] array)
+ {
+ return array.Length;
+ }
+
+ return actualValue.Count();
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/Collections/EnumerableEquivalentToAssertCondition.cs b/TUnit.Assertions/AssertConditions/Collections/EnumerableEquivalentToAssertCondition.cs
new file mode 100644
index 0000000000..902d1ee30f
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/Collections/EnumerableEquivalentToAssertCondition.cs
@@ -0,0 +1,25 @@
+using TUnit.Assertions.AssertConditions.Operators;
+
+namespace TUnit.Assertions.AssertConditions.Collections;
+
+public class EnumerableEquivalentToAssertCondition : AssertCondition, TAnd, TOr>
+ where TActual : IEnumerable
+ where TAnd : And, IAnd
+ where TOr : Or, IOr
+{
+ public EnumerableEquivalentToAssertCondition(AssertionBuilder assertionBuilder, IEnumerable expected) : base(assertionBuilder, expected)
+ {
+ }
+
+ protected override string DefaultMessage => "The two Enumerables were not equivalent";
+
+ protected internal override bool Passes(TActual? actualValue, Exception? exception)
+ {
+ if (actualValue is null && ExpectedValue is null)
+ {
+ return true;
+ }
+
+ return actualValue?.SequenceEqual(ExpectedValue!) ?? false;
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/Collections/PropertyOrMethodAccessor.cs b/TUnit.Assertions/AssertConditions/Collections/PropertyOrMethodAccessor.cs
new file mode 100644
index 0000000000..219cbc3afa
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/Collections/PropertyOrMethodAccessor.cs
@@ -0,0 +1,10 @@
+namespace TUnit.Assertions.AssertConditions.Collections;
+
+public class PropertyOrMethodAccessor
+{
+ public PropertyOrMethodAccessor()
+ {
+ }
+
+
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/ConnectorType.cs b/TUnit.Assertions/AssertConditions/ConnectorType.cs
new file mode 100644
index 0000000000..048182652a
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/ConnectorType.cs
@@ -0,0 +1,8 @@
+namespace TUnit.Assertions.AssertConditions;
+
+public enum ConnectorType
+{
+ None,
+ And,
+ Or
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/Connectors/AssertConditionAnd.cs b/TUnit.Assertions/AssertConditions/Connectors/AssertConditionAnd.cs
new file mode 100644
index 0000000000..787e1a12f8
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/Connectors/AssertConditionAnd.cs
@@ -0,0 +1,47 @@
+using TUnit.Assertions.AssertConditions.Operators;
+
+namespace TUnit.Assertions.AssertConditions.Connectors;
+
+public sealed class AssertConditionAnd : BaseAssertCondition
+ where TAnd : And, IAnd
+ where TOr : Or, IOr
+{
+ private readonly BaseAssertCondition _condition1;
+ private readonly BaseAssertCondition _condition2;
+
+ public AssertConditionAnd(BaseAssertCondition condition1, BaseAssertCondition condition2) : base(condition1.AssertionBuilder)
+ {
+ ArgumentNullException.ThrowIfNull(condition1);
+ ArgumentNullException.ThrowIfNull(condition2);
+
+ _condition1 = condition1;
+ _condition2 = condition2;
+ }
+
+ protected internal override string Message
+ {
+ get
+ {
+ var messages = new List(2);
+
+ if (!_condition1.Assert(ActualValue, Exception))
+ {
+ messages.Add(_condition1.Message);
+ }
+
+ if (!_condition2.Assert(ActualValue, Exception))
+ {
+ messages.Add(_condition2.Message);
+ }
+
+ return string.Join($"{Environment.NewLine} ", messages);
+ }
+ }
+
+ protected override string DefaultMessage => string.Empty;
+
+ protected internal override bool Passes(TActual? actualValue, Exception? exception)
+ {
+ return _condition1.Assert(actualValue, exception) && _condition2.Assert(actualValue, exception);
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/Connectors/AssertConditionOr.cs b/TUnit.Assertions/AssertConditions/Connectors/AssertConditionOr.cs
new file mode 100644
index 0000000000..ed39d5a1be
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/Connectors/AssertConditionOr.cs
@@ -0,0 +1,28 @@
+using TUnit.Assertions.AssertConditions.Operators;
+
+namespace TUnit.Assertions.AssertConditions.Connectors;
+
+public sealed class AssertConditionOr : BaseAssertCondition
+ where TAnd : And, IAnd
+ where TOr : Or, IOr
+{
+ private readonly BaseAssertCondition _condition1;
+ private readonly BaseAssertCondition _condition2;
+
+ public AssertConditionOr(BaseAssertCondition condition1, BaseAssertCondition condition2) : base(condition1.AssertionBuilder)
+ {
+ ArgumentNullException.ThrowIfNull(condition1);
+ ArgumentNullException.ThrowIfNull(condition2);
+
+ _condition1 = condition1;
+ _condition2 = condition2;
+ }
+
+ protected internal override string Message => $"{_condition1.Message} & {_condition2.Message}";
+ protected override string DefaultMessage => string.Empty;
+
+ protected internal override bool Passes(TActual? actualValue, Exception? exception)
+ {
+ return _condition1.Assert(actualValue, exception) || _condition2.Assert(actualValue, exception);
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/DelegateAssertCondition.cs b/TUnit.Assertions/AssertConditions/DelegateAssertCondition.cs
new file mode 100644
index 0000000000..f6367842f1
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/DelegateAssertCondition.cs
@@ -0,0 +1,26 @@
+using TUnit.Assertions.AssertConditions.Operators;
+
+namespace TUnit.Assertions.AssertConditions;
+
+public class DelegateAssertCondition : AssertCondition
+ where TAnd : And, IAnd
+ where TOr : Or, IOr
+{
+ private readonly Func _condition;
+
+ public DelegateAssertCondition(AssertionBuilder assertionBuilder,
+ TExpected? expected,
+ Func condition,
+ Func messageFactory) : base(assertionBuilder, expected)
+ {
+ _condition = condition;
+ WithMessage(messageFactory);
+ }
+
+ protected override string DefaultMessage => string.Empty;
+
+ protected internal override bool Passes(TActual? actualValue, Exception? exception)
+ {
+ return _condition(actualValue, ExpectedValue, exception);
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/FailureLocation.cs b/TUnit.Assertions/AssertConditions/FailureLocation.cs
new file mode 100644
index 0000000000..b9bd39ac5b
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/FailureLocation.cs
@@ -0,0 +1,8 @@
+namespace TUnit.Assertions.AssertConditions;
+
+public record FailureLocation
+{
+ public long Position { get; }
+ public object? ExpectedValue { get; }
+ public object? ActualValue { get; }
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/Generic/EqualsAssertCondition.cs b/TUnit.Assertions/AssertConditions/Generic/EqualsAssertCondition.cs
new file mode 100644
index 0000000000..b4417ad2c6
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/Generic/EqualsAssertCondition.cs
@@ -0,0 +1,16 @@
+using TUnit.Assertions.AssertConditions.Operators;
+
+namespace TUnit.Assertions.AssertConditions.Generic;
+
+public class EqualsAssertCondition(AssertionBuilder assertionBuilder, TActual expected)
+ : AssertCondition(assertionBuilder, expected)
+ where TAnd : And, IAnd
+ where TOr : Or, IOr
+{
+ protected override string DefaultMessage => $"Expected {ExpectedValue} but received {ActualValue}";
+
+ protected internal override bool Passes(TActual? actualValue, Exception? exception)
+ {
+ return Equals(actualValue, ExpectedValue);
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/Generic/Property.cs b/TUnit.Assertions/AssertConditions/Generic/Property.cs
new file mode 100644
index 0000000000..26ede218e2
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/Generic/Property.cs
@@ -0,0 +1,50 @@
+using TUnit.Assertions.AssertConditions.Connectors;
+using TUnit.Assertions.AssertConditions.Operators;
+
+namespace TUnit.Assertions.AssertConditions.Generic;
+
+public class Property : Property
+ where TAnd : And, IAnd
+ where TOr : Or, IOr
+{
+ public Property(string name, ConnectorType connectorType, BaseAssertCondition otherAssertConditions)
+ : base(name, connectorType, otherAssertConditions)
+ {
+ }
+
+ public Property(AssertionBuilder assertionBuilder, string name) : base(assertionBuilder, name)
+ {
+ }
+}
+
+public class Property(AssertionBuilder assertionBuilder, string name)
+ where TAnd : And, IAnd
+ where TOr : Or, IOr
+{
+ private readonly ConnectorType? _connectorType;
+ private readonly BaseAssertCondition? _otherAssertConditions;
+
+ public Property(string name, ConnectorType connectorType, BaseAssertCondition otherAssertConditions)
+ : this(otherAssertConditions.AssertionBuilder, name)
+ {
+ _connectorType = connectorType;
+ _otherAssertConditions = otherAssertConditions;
+ }
+
+ public BaseAssertCondition EqualTo(TExpected expected)
+ {
+ var assertCondition = new PropertyEqualsAssertCondition(assertionBuilder, name, expected);
+
+ if (_connectorType is ConnectorType.And)
+ {
+ return new AssertConditionAnd(_otherAssertConditions!, assertCondition);
+ }
+
+ if (_connectorType is ConnectorType.Or)
+ {
+ return new AssertConditionOr(_otherAssertConditions!, assertCondition);
+ }
+
+ return assertCondition;
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/Generic/PropertyEqualsAssertCondition.cs b/TUnit.Assertions/AssertConditions/Generic/PropertyEqualsAssertCondition.cs
new file mode 100644
index 0000000000..adfc149da4
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/Generic/PropertyEqualsAssertCondition.cs
@@ -0,0 +1,32 @@
+using TUnit.Assertions.AssertConditions.Operators;
+
+namespace TUnit.Assertions.AssertConditions.Generic;
+
+public class PropertyEqualsAssertCondition(AssertionBuilder assertionBuilder, string propertyName, TExpected expected)
+ : AssertCondition(assertionBuilder, expected)
+ where TAnd : And, IAnd
+ where TOr : Or, IOr
+{
+ protected override string DefaultMessage => $"Expected {ExpectedValue} but received {ActualValue}";
+
+ protected internal override bool Passes(TActual? actualValue, Exception? exception)
+ {
+ var propertyValue = GetPropertyValue(actualValue);
+
+ WithMessage((_, _) => $"Expected {ExpectedValue} but received {propertyValue}");
+
+ return Equals(propertyValue, ExpectedValue);
+ }
+
+ private object? GetPropertyValue(object? actualValue)
+ {
+ ArgumentNullException.ThrowIfNull(actualValue);
+
+ if (actualValue.GetType().GetProperty(propertyName) is null)
+ {
+ throw new ArgumentException($"No {propertyName} property or method was found on {actualValue.GetType().Name}");
+ }
+
+ return actualValue.GetPropertyValue(propertyName);
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/Generic/PropertyOrMethod.cs b/TUnit.Assertions/AssertConditions/Generic/PropertyOrMethod.cs
new file mode 100644
index 0000000000..ee29fb216e
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/Generic/PropertyOrMethod.cs
@@ -0,0 +1,50 @@
+using TUnit.Assertions.AssertConditions.Connectors;
+using TUnit.Assertions.AssertConditions.Operators;
+
+namespace TUnit.Assertions.AssertConditions.Generic;
+
+public class PropertyOrMethod : Property
+ where TAnd : And, IAnd
+ where TOr : Or, IOr
+{
+ public PropertyOrMethod(string name, ConnectorType connectorType, BaseAssertCondition otherAssertConditions)
+ : base(name, connectorType, otherAssertConditions)
+ {
+ }
+
+ public PropertyOrMethod(AssertionBuilder assertionBuilder, string name) : base(assertionBuilder, name)
+ {
+ }
+}
+
+public class PropertyOrMethod(AssertionBuilder assertionBuilder, string name)
+ where TAnd : And, IAnd
+ where TOr : Or, IOr
+{
+ private readonly ConnectorType? _connectorType;
+ private readonly BaseAssertCondition? _otherAssertConditions;
+
+ public PropertyOrMethod(string name, ConnectorType connectorType, BaseAssertCondition otherAssertConditions)
+ : this(otherAssertConditions.AssertionBuilder, name)
+ {
+ _connectorType = connectorType;
+ _otherAssertConditions = otherAssertConditions;
+ }
+
+ public BaseAssertCondition EqualTo(TExpected expected)
+ {
+ var assertCondition = new PropertyEqualsAssertCondition(assertionBuilder, name, expected);
+
+ if (_connectorType is ConnectorType.And)
+ {
+ return new AssertConditionAnd(_otherAssertConditions!, assertCondition);
+ }
+
+ if (_connectorType is ConnectorType.Or)
+ {
+ return new AssertConditionOr(_otherAssertConditions!, assertCondition);
+ }
+
+ return assertCondition;
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/Generic/PropertyOrMethodEqualsAssertCondition.cs b/TUnit.Assertions/AssertConditions/Generic/PropertyOrMethodEqualsAssertCondition.cs
new file mode 100644
index 0000000000..5432bd2f5d
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/Generic/PropertyOrMethodEqualsAssertCondition.cs
@@ -0,0 +1,33 @@
+using TUnit.Assertions.AssertConditions.Operators;
+
+namespace TUnit.Assertions.AssertConditions.Generic;
+
+public class PropertyOrMethodEqualsAssertCondition(AssertionBuilder assertionBuilder, string propertyName, TExpected expected)
+ : AssertCondition(assertionBuilder, expected)
+ where TAnd : And, IAnd
+ where TOr : Or, IOr
+{
+ protected override string DefaultMessage => $"Expected {ExpectedValue} but received {ActualValue}";
+
+ protected internal override bool Passes(TActual? actualValue, Exception? exception)
+ {
+ var propertyValue = GetPropertyValue(actualValue);
+
+ WithMessage((_, _) => $"Expected {ExpectedValue} but received {propertyValue}");
+
+ return Equals(propertyValue, ExpectedValue);
+ }
+
+ private object? GetPropertyValue(object? actualValue)
+ {
+ ArgumentNullException.ThrowIfNull(actualValue);
+
+ if (actualValue.GetType().GetProperty(propertyName) is null
+ && actualValue.GetType().GetProperty(propertyName) is null)
+ {
+ throw new ArgumentException($"No {propertyName} property or method was found on {actualValue.GetType().Name}");
+ }
+
+ return actualValue.GetPropertyValue(propertyName) ?? actualValue.GetMethodReturnValue(propertyName);
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/Generic/SameReferenceAssertCondition.cs b/TUnit.Assertions/AssertConditions/Generic/SameReferenceAssertCondition.cs
new file mode 100644
index 0000000000..25f537ef8c
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/Generic/SameReferenceAssertCondition.cs
@@ -0,0 +1,20 @@
+using TUnit.Assertions.AssertConditions.Operators;
+
+namespace TUnit.Assertions.AssertConditions.Generic;
+
+public class SameReferenceAssertCondition : AssertCondition
+ where TAnd : And, IAnd
+ where TOr : Or, IOr
+{
+
+ public SameReferenceAssertCondition(AssertionBuilder assertionBuilder, TExpected expected) : base(assertionBuilder, expected)
+ {
+ }
+
+ protected override string DefaultMessage => "The two objects are different references.";
+
+ protected internal override bool Passes(TActual? actualValue, Exception? exception)
+ {
+ return ReferenceEquals(actualValue, ExpectedValue);
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/Generic/TypeOfAssertCondition.cs b/TUnit.Assertions/AssertConditions/Generic/TypeOfAssertCondition.cs
new file mode 100644
index 0000000000..9f6c668547
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/Generic/TypeOfAssertCondition.cs
@@ -0,0 +1,16 @@
+using TUnit.Assertions.AssertConditions.Operators;
+
+namespace TUnit.Assertions.AssertConditions.Generic;
+
+public class TypeOfAssertCondition(AssertionBuilder assertionBuilder)
+ : AssertCondition(assertionBuilder, default)
+ where TAnd : And, IAnd
+ where TOr : Or, IOr
+{
+ protected override string DefaultMessage => $"{ActualValue} is {ActualValue?.GetType().Name ?? "null"} instead of {typeof(TExpected).Name}";
+
+ protected internal override bool Passes(TActual? actualValue, Exception? exception)
+ {
+ return actualValue?.GetType() == typeof(TExpected);
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/NullAssertCondition.cs b/TUnit.Assertions/AssertConditions/NullAssertCondition.cs
new file mode 100644
index 0000000000..2dbc472563
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/NullAssertCondition.cs
@@ -0,0 +1,18 @@
+using TUnit.Assertions.AssertConditions.Operators;
+
+namespace TUnit.Assertions.AssertConditions;
+
+public class NullAssertCondition : AssertCondition
+ where TAnd : And, IAnd
+ where TOr : Or, IOr
+{
+ public NullAssertCondition(AssertionBuilder assertionBuilder) : base(assertionBuilder, default)
+ {
+ }
+
+ protected override string DefaultMessage => $"{ActualValue} is not null";
+ protected internal override bool Passes(TActual? actualValue, Exception? exception)
+ {
+ return actualValue is null;
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/Operators/And.cs b/TUnit.Assertions/AssertConditions/Operators/And.cs
new file mode 100644
index 0000000000..b31fb3d0d7
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/Operators/And.cs
@@ -0,0 +1,13 @@
+namespace TUnit.Assertions.AssertConditions.Operators;
+
+public abstract class And
+ where TAnd : And, IAnd
+ where TOr : Or, IOr
+{
+ protected readonly BaseAssertCondition OtherAssertCondition;
+
+ public And(BaseAssertCondition otherAssertCondition)
+ {
+ OtherAssertCondition = otherAssertCondition;
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/Operators/AssertionType.cs b/TUnit.Assertions/AssertConditions/Operators/AssertionType.cs
new file mode 100644
index 0000000000..6aab074378
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/Operators/AssertionType.cs
@@ -0,0 +1,8 @@
+namespace TUnit.Assertions.AssertConditions.Operators;
+
+[Flags]
+public enum AssertionType
+{
+ Value = 1,
+ Delegate = 2
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/Operators/DelegateAnd.cs b/TUnit.Assertions/AssertConditions/Operators/DelegateAnd.cs
new file mode 100644
index 0000000000..a029569ad3
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/Operators/DelegateAnd.cs
@@ -0,0 +1,18 @@
+namespace TUnit.Assertions.AssertConditions.Operators;
+
+public class DelegateAnd
+ : And, DelegateOr>,
+ IDelegateAssertions, DelegateOr>,
+ IAnd, TActual, DelegateAnd, DelegateOr>
+{
+ public DelegateAnd(BaseAssertCondition, DelegateOr> otherAssertCondition) : base(otherAssertCondition)
+ {
+ }
+
+ public Throws, DelegateOr> Throws => new(OtherAssertCondition.AssertionBuilder, ConnectorType.And, OtherAssertCondition);
+
+ public static DelegateAnd Create(BaseAssertCondition, DelegateOr> otherAssertCondition)
+ {
+ return new DelegateAnd(otherAssertCondition)!;
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/Operators/DelegateOr.cs b/TUnit.Assertions/AssertConditions/Operators/DelegateOr.cs
new file mode 100644
index 0000000000..6d602d240d
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/Operators/DelegateOr.cs
@@ -0,0 +1,18 @@
+namespace TUnit.Assertions.AssertConditions.Operators;
+
+public class DelegateOr
+ : Or, DelegateOr>,
+ IDelegateAssertions, DelegateOr>,
+ IOr, TActual, DelegateAnd, DelegateOr>
+{
+ public DelegateOr(BaseAssertCondition, DelegateOr> otherAssertCondition) : base(otherAssertCondition)
+ {
+ }
+
+ public Throws, DelegateOr> Throws => new(OtherAssertCondition.AssertionBuilder, ConnectorType.Or, OtherAssertCondition);
+
+ public static DelegateOr Create(BaseAssertCondition, DelegateOr> otherAssertCondition)
+ {
+ return new DelegateOr(otherAssertCondition);
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/Operators/IAnd.cs b/TUnit.Assertions/AssertConditions/Operators/IAnd.cs
new file mode 100644
index 0000000000..e4b51262ea
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/Operators/IAnd.cs
@@ -0,0 +1,8 @@
+namespace TUnit.Assertions.AssertConditions.Operators;
+
+public interface IAnd
+ where TAnd : And, IAnd
+ where TOr : Or, IOr
+{
+ public static abstract TSelf Create(BaseAssertCondition otherAssertCondition);
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/Operators/IDelegateAssertions.cs b/TUnit.Assertions/AssertConditions/Operators/IDelegateAssertions.cs
new file mode 100644
index 0000000000..1c911f2ca8
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/Operators/IDelegateAssertions.cs
@@ -0,0 +1,8 @@
+namespace TUnit.Assertions.AssertConditions.Operators;
+
+internal interface IDelegateAssertions
+ where TAnd : And, IAnd
+ where TOr : Or, IOr
+{
+ public Throws Throws { get; }
+}
\ No newline at end of file
diff --git a/TUnit.Assertions/AssertConditions/Operators/IOr.cs b/TUnit.Assertions/AssertConditions/Operators/IOr.cs
new file mode 100644
index 0000000000..5147bbd9b7
--- /dev/null
+++ b/TUnit.Assertions/AssertConditions/Operators/IOr.cs
@@ -0,0 +1,8 @@
+namespace TUnit.Assertions.AssertConditions.Operators;
+
+public interface IOr