Skip to content

Commit 4fb7d75

Browse files
committed
Add xUnit3003 (FactAttribute-drived constructor requirement)
1 parent d8765de commit 4fb7d75

File tree

9 files changed

+199
-17
lines changed

9 files changed

+199
-17
lines changed

src/Directory.Build.props

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@
7878
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
7979
<PackageReference Include="NSubstitute" Version="5.3.0" />
8080
<PackageReference Include="System.ValueTuple" Version="4.6.1" />
81-
<PackageReference Include="xunit.v3.assert.source" Version="2.0.3-pre.35" />
82-
<PackageReference Include="xunit.v3.core" Version="2.0.3-pre.35" />
83-
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.0" />
81+
<PackageReference Include="xunit.v3.assert.source" Version="3.0.0-pre.14" />
82+
<PackageReference Include="xunit.v3.core" Version="3.0.0-pre.14" />
83+
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.1" />
8484
</ItemGroup>
8585

8686
</When>
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CSharp;
5+
using Xunit;
6+
using Xunit.Analyzers;
7+
using Verify = CSharpVerifier<Xunit.Analyzers.FactAttributeDerivedClassesShouldProvideSourceInformationConstructor>;
8+
using Verify_v3_Pre300 = CSharpVerifier<FactAttributeDerivedClassesShouldProvideSourceInformationConstructorTests.Analyzer_v3_Pre300>;
9+
10+
public class FactAttributeDerivedClassesShouldProvideSourceInformationConstructorTests
11+
{
12+
[Fact]
13+
public async Task v2_OlderV3_DoesNotTrigger()
14+
{
15+
var code = /* lang=c#-test */ """
16+
using Xunit;
17+
18+
public class MyFactAttribute : FactAttribute { }
19+
20+
public class MyTheoryAttribute : TheoryAttribute { }
21+
""";
22+
23+
await Verify.VerifyAnalyzerV2(code);
24+
await Verify_v3_Pre300.VerifyAnalyzerV3(code);
25+
}
26+
27+
28+
[Fact]
29+
public async Task v3_Triggers()
30+
{
31+
#if ROSLYN_LATEST
32+
var code = /* lang=c#-test */ """
33+
using System.Runtime.CompilerServices;
34+
using Xunit;
35+
36+
public class {|xUnit3003:MyFactAttribute|} : FactAttribute { }
37+
38+
public class MyFactWithCtorArgs([CallerFilePath] string? foo = null, [CallerLineNumber] int bar = -1)
39+
: FactAttribute(foo, bar)
40+
{ }
41+
42+
public class {|xUnit3003:MyTheoryAttribute|} : TheoryAttribute { }
43+
44+
public class MyTheoryWithCtorArgs(
45+
int x,
46+
string y,
47+
[CallerFilePath] string? sourceFilePath = null,
48+
[CallerLineNumber] int sourceLineNumber = -1)
49+
: TheoryAttribute(sourceFilePath, sourceLineNumber)
50+
{ }
51+
""";
52+
53+
await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp12, code);
54+
#else
55+
var code = /* lang=c#-test */ """
56+
using System.Runtime.CompilerServices;
57+
using Xunit;
58+
59+
public class {|xUnit3003:MyFactAttribute|} : FactAttribute { }
60+
61+
public class MyFactWithCtorArgs : FactAttribute
62+
{
63+
public MyFactWithCtorArgs([CallerFilePath] string? foo = null, [CallerLineNumber] int bar = -1)
64+
: base(foo, bar)
65+
{ }
66+
}
67+
68+
public class {|xUnit3003:MyTheoryAttribute|} : TheoryAttribute { }
69+
70+
public class MyTheoryWithCtorArgs : TheoryAttribute
71+
{
72+
public MyTheoryWithCtorArgs(int x, string y, [CallerFilePath] string? foo = null, [CallerLineNumber] int bar = -1)
73+
: base(foo, bar)
74+
{ }
75+
}
76+
""";
77+
78+
await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp8, code);
79+
#endif
80+
}
81+
82+
internal class Analyzer_v3_Pre300 : FactAttributeDerivedClassesShouldProvideSourceInformationConstructor
83+
{
84+
protected override XunitContext CreateXunitContext(Compilation compilation) =>
85+
XunitContext.ForV3(compilation, new Version(2, 999, 999));
86+
}
87+
}

src/xunit.analyzers.tests/Utility/CodeAnalyzerHelper.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,10 @@ static CodeAnalyzerHelper()
6161
new PackageIdentity("Microsoft.Extensions.Primitives", "8.0.0"),
6262
new PackageIdentity("System.Threading.Tasks.Extensions", "4.5.4"),
6363
new PackageIdentity("System.Text.Json", "8.0.0"),
64-
new PackageIdentity("xunit.v3.assert", "2.0.3-pre.35"),
65-
new PackageIdentity("xunit.v3.common", "2.0.3-pre.35"),
66-
new PackageIdentity("xunit.v3.extensibility.core", "2.0.3-pre.35"),
67-
new PackageIdentity("xunit.v3.runner.common", "2.0.3-pre.35")
64+
new PackageIdentity("xunit.v3.assert", "3.0.0-pre.14"),
65+
new PackageIdentity("xunit.v3.common", "3.0.0-pre.14"),
66+
new PackageIdentity("xunit.v3.extensibility.core", "3.0.0-pre.14"),
67+
new PackageIdentity("xunit.v3.runner.common", "3.0.0-pre.14")
6868
)
6969
);
7070

@@ -74,8 +74,8 @@ static CodeAnalyzerHelper()
7474
new PackageIdentity("Microsoft.Extensions.Primitives", "8.0.0"),
7575
new PackageIdentity("System.Threading.Tasks.Extensions", "4.5.4"),
7676
new PackageIdentity("System.Text.Json", "8.0.0"),
77-
new PackageIdentity("xunit.v3.common", "2.0.3-pre.35"),
78-
new PackageIdentity("xunit.v3.runner.utility", "2.0.3-pre.35")
77+
new PackageIdentity("xunit.v3.common", "3.0.0-pre.14"),
78+
new PackageIdentity("xunit.v3.runner.utility", "3.0.0-pre.14")
7979
)
8080
);
8181
}

src/xunit.analyzers.tests/xunit.analyzers.tests.csproj

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@
2323
<PackageDownload Include="xunit.extensibility.execution" Version="[2.9.3-pre.4]" />
2424
<PackageDownload Include="xunit.runner.utility" Version="[2.9.3-pre.4]" />
2525

26-
<PackageDownload Include="xunit.v3.assert" Version="[2.0.3-pre.35]" />
27-
<PackageDownload Include="xunit.v3.common" Version="[2.0.3-pre.35]" />
28-
<PackageDownload Include="xunit.v3.extensibility.core" Version="[2.0.3-pre.35]" />
29-
<PackageDownload Include="xunit.v3.runner.common" Version="[2.0.3-pre.35]" />
30-
<PackageDownload Include="xunit.v3.runner.utility" Version="[2.0.3-pre.35]" />
26+
<PackageDownload Include="xunit.v3.assert" Version="[3.0.0-pre.14]" />
27+
<PackageDownload Include="xunit.v3.common" Version="[3.0.0-pre.14]" />
28+
<PackageDownload Include="xunit.v3.extensibility.core" Version="[3.0.0-pre.14]" />
29+
<PackageDownload Include="xunit.v3.runner.common" Version="[3.0.0-pre.14]" />
30+
<PackageDownload Include="xunit.v3.runner.utility" Version="[3.0.0-pre.14]" />
3131

3232
<!-- Download packages referenced by CodeAnalysisNetAnalyzers -->
3333
<PackageDownload Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="[9.0.0-preview.24454.1]" />

src/xunit.analyzers/Utility/Descriptors.xUnit3xxx.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,14 @@ public static partial class Descriptors
3333
"Class {0} is JSON serializable and should not be tested for its concrete type. Test for its primary interface instead."
3434
);
3535

36-
// Placeholder for rule X3003
36+
public static DiagnosticDescriptor X3003_ProvideConstructorForFactAttributeOverride { get; } =
37+
Diagnostic(
38+
"xUnit3003",
39+
"Classes which extend FactAttribute (directly or indirectly) should provide a public constructor for source information",
40+
Extensibility,
41+
Warning,
42+
"Class {0} extends FactAttribute. It should include a public constructor for source information."
43+
);
3744

3845
// Placeholder for rule X3004
3946

src/xunit.analyzers/Utility/TypeSymbolFactory.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ public static class TypeSymbolFactory
3030
public static INamedTypeSymbol? BigInteger(Compilation compilation) =>
3131
Guard.ArgumentNotNull(compilation).GetTypeByMetadataName("System.Numerics.BigInteger");
3232

33+
public static INamedTypeSymbol? CallerFilePathAttribute(Compilation compilation) =>
34+
Guard.ArgumentNotNull(compilation).GetTypeByMetadataName("System.Runtime.CompilerServices.CallerFilePathAttribute");
35+
36+
public static INamedTypeSymbol? CallerLineNumberAttribute(Compilation compilation) =>
37+
Guard.ArgumentNotNull(compilation).GetTypeByMetadataName("System.Runtime.CompilerServices.CallerLineNumberAttribute");
38+
3339
public static INamedTypeSymbol? CancellationToken(Compilation compilation) =>
3440
Guard.ArgumentNotNull(compilation).GetTypeByMetadataName("System.Threading.CancellationToken");
3541

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System;
2+
using System.Linq;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.Diagnostics;
5+
6+
namespace Xunit.Analyzers;
7+
8+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
9+
public class FactAttributeDerivedClassesShouldProvideSourceInformationConstructor() :
10+
XunitV3DiagnosticAnalyzer(Descriptors.X3003_ProvideConstructorForFactAttributeOverride)
11+
{
12+
static readonly Version Version_3_0_0 = new(3, 0, 0);
13+
14+
public override void AnalyzeCompilation(
15+
CompilationStartAnalysisContext context,
16+
XunitContext xunitContext)
17+
{
18+
Guard.ArgumentNotNull(context);
19+
Guard.ArgumentNotNull(xunitContext);
20+
21+
var factAttributeType = xunitContext.Core.FactAttributeType;
22+
if (factAttributeType is null)
23+
return;
24+
25+
var callerFilePathAttribute = TypeSymbolFactory.CallerFilePathAttribute(context.Compilation);
26+
if (callerFilePathAttribute is null)
27+
return;
28+
29+
var callerLineNumberAttribute = TypeSymbolFactory.CallerLineNumberAttribute(context.Compilation);
30+
if (callerLineNumberAttribute is null)
31+
return;
32+
33+
context.RegisterSymbolAction(context =>
34+
{
35+
var type = context.Symbol as INamedTypeSymbol;
36+
if (type is null)
37+
return;
38+
39+
var baseType = type.BaseType;
40+
while (true)
41+
{
42+
if (baseType is null)
43+
return;
44+
45+
if (SymbolEqualityComparer.Default.Equals(factAttributeType, baseType))
46+
break;
47+
48+
baseType = baseType.BaseType;
49+
}
50+
51+
if (type.Constructors.Any(hasSourceInformationParameters))
52+
return;
53+
54+
context.ReportDiagnostic(
55+
Diagnostic.Create(
56+
Descriptors.X3003_ProvideConstructorForFactAttributeOverride,
57+
type.Locations.First()
58+
)
59+
);
60+
}, SymbolKind.NamedType);
61+
62+
bool hasSourceInformationParameters(IMethodSymbol symbol)
63+
{
64+
var hasCallerFilePath = false;
65+
var hasCallerLineNumber = false;
66+
67+
foreach (var parameter in symbol.Parameters)
68+
foreach (var attribute in parameter.GetAttributes().Select(a => a.AttributeClass))
69+
{
70+
if (SymbolEqualityComparer.Default.Equals(callerFilePathAttribute, attribute))
71+
hasCallerFilePath = true;
72+
if (SymbolEqualityComparer.Default.Equals(callerLineNumberAttribute, attribute))
73+
hasCallerLineNumber = true;
74+
}
75+
76+
return hasCallerFilePath && hasCallerLineNumber;
77+
}
78+
}
79+
80+
protected override bool ShouldAnalyze(XunitContext xunitContext) =>
81+
xunitContext?.V3Core is not null && xunitContext.V3Core.Version >= Version_3_0_0;
82+
}

tools/builder/build.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
</ItemGroup>
1717

1818
<ItemGroup>
19-
<PackageDownload Include="xunit.v3.runner.console" Version="[2.0.3-pre.35]" />
19+
<PackageDownload Include="xunit.v3.runner.console" Version="[3.0.0-pre.14]" />
2020
</ItemGroup>
2121

2222
</Project>

tools/builder/models/BuildContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public partial IReadOnlyList<string> GetSkippedAnalysisFolders() =>
1616

1717
partial void Initialize()
1818
{
19-
consoleRunner = Path.Combine(NuGetPackageCachePath, "xunit.v3.runner.console", "2.0.3-pre.35", "tools", "net472", "xunit.v3.runner.console.exe");
19+
consoleRunner = Path.Combine(NuGetPackageCachePath, "xunit.v3.runner.console", "3.0.0-pre.14", "tools", "net472", "xunit.v3.runner.console.exe");
2020
if (!File.Exists(consoleRunner))
2121
throw new InvalidOperationException($"Cannot find console runner at '{consoleRunner}'");
2222
}

0 commit comments

Comments
 (0)