Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
543e79d
feat: add TUnit.Assertions.Should package
thomhurst Apr 28, 2026
5b89e90
fix: address PR #5785 review feedback
thomhurst Apr 28, 2026
32bdd5d
feat: discover [GenerateAssertion]/[AssertionFrom]/hand-written exten…
thomhurst Apr 28, 2026
b4f2494
ci: wire TUnit.Assertions.Should into pipeline as beta package
thomhurst Apr 28, 2026
148f72f
refactor: address /simplify review findings
thomhurst Apr 28, 2026
584bfb4
feat: source-generate ShouldCollectionSource instance methods
thomhurst Apr 28, 2026
d121ad1
feat(analyzers): extend existing analyzers to recognise Should syntax
thomhurst Apr 28, 2026
0d59c81
refactor(analyzers): align Should-aware analyzer code with existing p…
thomhurst Apr 28, 2026
9f0460e
docs: add Should syntax page
thomhurst Apr 28, 2026
2830fed
fix: address PR review — CI sln, generator tests, ShouldNameAttribute
thomhurst Apr 28, 2026
8f88cc4
fix: address PR round 5 — docs example + simplify NameConjugator
thomhurst Apr 28, 2026
dd7931a
chore: round 6 cleanup — drop unused packages, kill empty if block
thomhurst Apr 28, 2026
2273360
fix(should-gen): handle -es endings in name conjugator
thomhurst Apr 28, 2026
4c54f08
test(should-gen): add Verify snapshots for generator output
thomhurst Apr 28, 2026
33a903e
fix(should): render generic exception type names without backtick man…
thomhurst Apr 28, 2026
ce5dc38
docs(should): clarify analyzer extension match, Clear() coverage, Doe…
thomhurst Apr 29, 2026
a3cba8e
perf(should-gen): cache per-reference walk by MetadataReference
thomhurst Apr 29, 2026
f8f0fa6
fix(analyzer-tests): load netstandard2.0 builds to avoid CS1705 silen…
thomhurst Apr 29, 2026
6bd1516
fix(should-gen): swap reference cache to ConditionalWeakTable to avoi…
thomhurst Apr 29, 2026
8a726e9
ci(pipeline): wire Should test modules into the release gate
thomhurst Apr 29, 2026
10b0a15
fix(assertions): IsAll -> IsFullRange; propagate Obsolete/EditorBrows…
thomhurst Apr 29, 2026
4769882
refactor(analyzer-tests): centralize GetCompatibleDllPath; document a…
thomhurst Apr 29, 2026
4f9f779
refactor(should): drop ExpressionBuilder.Clear coupling; centralize g…
thomhurst Apr 29, 2026
46ba85a
refactor: drop redundant GetCompatibleDllPath wrappers; document roun…
thomhurst Apr 29, 2026
de03ec8
fix(should): address review edge cases
thomhurst Apr 29, 2026
8d5b321
fix(should): handle z-ending verbs
thomhurst Apr 29, 2026
4fa90f1
fix(should): address review blockers
thomhurst Apr 29, 2026
d5be784
test(should): cover assertion failures
thomhurst Apr 29, 2026
5ad8a66
test(should): cover failing entry shapes
thomhurst Apr 29, 2026
37df1c8
fix(ci): align analyzer test references
thomhurst Apr 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net472;net8.0;net9.0;net10.0</TargetFrameworks>
</PropertyGroup>

<Import Project="..\TestProject.props" />

<PropertyGroup>
<NoWarn>$(NoWarn);MSB3277</NoWarn>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\TUnit.Assertions.Should.SourceGenerator\TUnit.Assertions.Should.SourceGenerator.csproj" />
<ProjectReference Include="..\TUnit.Assertions.Should\TUnit.Assertions.Should.csproj" />
<ProjectReference Include="..\TUnit.Assertions\TUnit.Assertions.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.SourceGenerators.Testing" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" />
<PackageReference Include="Microsoft.Testing.Extensions.HangDump" />
<PackageReference Include="NuGet.Protocol" />
<PackageReference Include="Sourcy.DotNet">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Sourcy.Git">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Verify" />
<PackageReference Include="Verify.TUnit" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<Import Project="..\TestProject.targets" />
</Project>
79 changes: 79 additions & 0 deletions TUnit.Assertions.Should.SourceGenerator/NameConjugator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
namespace TUnit.Assertions.Should.SourceGenerator;

/// <summary>
/// Pure-function transformer from <c>Is*</c>/<c>Has*</c>/<c>Does*</c>/3rd-person-singular method
/// names produced by <c>[AssertionExtension]</c> to FluentAssertions-style imperative
/// Should-flavored equivalents (<c>BeEqualTo</c>, <c>HaveCount</c>, <c>NotContain</c>, etc.).
/// </summary>
internal static class NameConjugator
{
/// <summary>
/// Conjugate an <c>[AssertionExtension]</c> method name into its Should-flavored counterpart.
/// Returns the conjugated name plus a flag indicating whether a known rule was applied.
/// When <c>Matched</c> is false the original name is returned unchanged so the caller can
/// emit a TUSHOULD001 diagnostic.
/// </summary>
public static (string Name, bool Matched) Conjugate(string methodName)
{
if (string.IsNullOrEmpty(methodName))
{
return (methodName, false);
}

// Order: longest-prefix-first so IsNot/DoesNot win over Is/Does.
if (TryReplacePrefix(methodName, "IsNot", "NotBe", out var s)) return (s, true);
if (TryReplacePrefix(methodName, "Is", "Be", out s)) return (s, true);
if (TryReplacePrefix(methodName, "Has", "Have", out s)) return (s, true);
if (TryReplacePrefix(methodName, "DoesNot", "Not", out s)) return (s, true);
if (TryReplacePrefix(methodName, "Does", "", out s)) return (s, true);
if (TryDropTrailingS(methodName, out s)) return (s, true);

return (methodName, false);
}

/// <summary>
/// Replaces <paramref name="prefix"/> with <paramref name="replacement"/> only when it ends
/// at a CamelCase word boundary (next char uppercase, or end of name). Prevents matches like
/// "Issue" → "Besue".
/// </summary>
private static bool TryReplacePrefix(string name, string prefix, string replacement, out string result)
{
if (!name.StartsWith(prefix, System.StringComparison.Ordinal)
|| (name.Length > prefix.Length && !char.IsUpper(name[prefix.Length])))
{
result = name;
return false;
}

result = replacement + name.Substring(prefix.Length);
return true;
}

/// <summary>
/// First-word -s drop. <c>Contains</c>→<c>Contain</c>, <c>StartsWith</c>→<c>StartWith</c>.
/// Skips <c>-ss</c> endings so <c>Pass</c> stays <c>Pass</c>.
/// </summary>
private static bool TryDropTrailingS(string name, out string result)
{
var firstWordEnd = name.Length;
for (var i = 1; i < name.Length; i++)
{
if (char.IsUpper(name[i]))
{
firstWordEnd = i;
break;
}
}

if (firstWordEnd < 2
|| name[firstWordEnd - 1] != 's'
|| name[firstWordEnd - 2] == 's')
{
result = name;
return false;
}

result = name.Substring(0, firstWordEnd - 1) + name.Substring(firstWordEnd);
return true;
}
}
Loading
Loading