diff --git a/README.md b/README.md
index 93248ba..025ab84 100644
--- a/README.md
+++ b/README.md
@@ -38,7 +38,7 @@ The source generator will produce classes containing the matching unit tests.
```csharp
// shortened code for readability
-[GeneratedCode("Sungaila.InlineTest", "0.9.0-preview+17ac90a4b0b471c88edc5fcedee4124a7cbbac28")]
+[GeneratedCode("Sungaila.InlineTest", "1.0.0+17ac90a4b0b471c88edc5fcedee4124a7cbbac28")]
[TestClass]
public partial class ReadmeExampleTests
{
@@ -71,7 +71,8 @@ public partial class ReadmeExampleTests
## Restrictions
1. The same Attribute rules apply here. Your parameters have to be either
- a constant value
- - a `System.Type`
+ - a `System.Type` (defined at compile-time)
- a single-dimensional array of the above
-2. The annotated method must be `static` or the declaring class must have a parameterless constructor.
-3. The annotated method must not have more than 16 parameters.
+2. The method must not have more than 15 parameters.
+3. The method must be defined inside a `class` or `struct`.
+4. For classes without a parameterless constructor the method must be `static`.
\ No newline at end of file
diff --git a/src/InlineTest.Tests/InlineTest.Tests.csproj b/src/InlineTest.Tests/InlineTest.Tests.csproj
index f8985a2..27d44d0 100644
--- a/src/InlineTest.Tests/InlineTest.Tests.csproj
+++ b/src/InlineTest.Tests/InlineTest.Tests.csproj
@@ -13,10 +13,11 @@
enable
strict
nullable
+ CA1822,CA2208,IDE0060
-
+
\ No newline at end of file
diff --git a/src/InlineTest.Tests/MethodOverloadTests.cs b/src/InlineTest.Tests/MethodOverloadTests.cs
new file mode 100644
index 0000000..bc8a097
--- /dev/null
+++ b/src/InlineTest.Tests/MethodOverloadTests.cs
@@ -0,0 +1,17 @@
+namespace Sungaila.InlineTest.Tests
+{
+ public class MethodOverloadTests
+ {
+ [AreEqual(Expected = (string?)null)]
+ public static string? EchoValue() => null;
+
+ [AreEqual("Moin Moin", Expected = "Moin Moin")]
+ public static string? EchoValue(string input) => input;
+
+ [AreEqual("Moin Moin", Expected = "Moin Moin")]
+ public static object? EchoValue(object input) => input;
+
+ [AreEqual(1994, Expected = 1994)]
+ public static int EchoValue(int input) => input;
+ }
+}
\ No newline at end of file
diff --git a/src/InlineTest.Tests/ReadmeExample.cs b/src/InlineTest.Tests/ReadmeExample.cs
index d3124d4..0f1ed44 100644
--- a/src/InlineTest.Tests/ReadmeExample.cs
+++ b/src/InlineTest.Tests/ReadmeExample.cs
@@ -23,7 +23,7 @@ public int Divide(int dividend, int divisor)
public void CheckForGeneratedTests()
{
// find the generated test class
- var type = typeof(ReadmeExample).Assembly.GetType("Sungaila.InlineTest.Generated.ReadmeExampleTests", true) ?? throw new NullReferenceException();
+ var type = typeof(ReadmeExample).Assembly.GetType("Sungaila.InlineTest.Generated.Sungaila_InlineTest_Tests_ReadmeExample", true) ?? throw new NullReferenceException();
// there must be a TestClassAttribute
Assert.IsNotNull(type.GetCustomAttribute());
diff --git a/src/InlineTest.Tests/StructTests.cs b/src/InlineTest.Tests/StructTests.cs
new file mode 100644
index 0000000..ba04fbe
--- /dev/null
+++ b/src/InlineTest.Tests/StructTests.cs
@@ -0,0 +1,51 @@
+namespace Sungaila.InlineTest.Tests
+{
+ public struct StructTests
+ {
+ // structs must have a parameterless constructor
+ // so this dummy constructor should be fine
+ public StructTests(object? something) { }
+
+ [AreEqual("Moin Moin", Expected = "Moin Moin")]
+ public string? EchoValueInstance(string input) => input;
+
+ [AreEqual("Moin Moin", Expected = "Moin Moin")]
+ public static string? EchoValueStatic(string input) => input;
+
+ public struct SubStructDepth1
+ {
+ [AreEqual("Moin Moin", Expected = "Moin Moin")]
+ public string? EchoValueInstance(string input) => input;
+
+ [AreEqual("Moin Moin", Expected = "Moin Moin")]
+ public static string? EchoValueStatic(string input) => input;
+
+ public struct SubStructDepth2
+ {
+ [AreEqual("Moin Moin", Expected = "Moin Moin")]
+ public string? EchoValueInstance(string input) => input;
+
+ [AreEqual("Moin Moin", Expected = "Moin Moin")]
+ public static string? EchoValueStatic(string input) => input;
+
+ public struct SubStructDepth3
+ {
+ [AreEqual("Moin Moin", Expected = "Moin Moin")]
+ public string? EchoValueInstance(string input) => input;
+
+ [AreEqual("Moin Moin", Expected = "Moin Moin")]
+ public static string? EchoValueStatic(string input) => input;
+
+ public struct SubStructDepth4
+ {
+ [AreEqual("Moin Moin", Expected = "Moin Moin")]
+ public string? EchoValueInstance(string input) => input;
+
+ [AreEqual("Moin Moin", Expected = "Moin Moin")]
+ public static string? EchoValueStatic(string input) => input;
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/InlineTest.Tests/SubClassTests.cs b/src/InlineTest.Tests/SubClassTests.cs
new file mode 100644
index 0000000..6f3f386
--- /dev/null
+++ b/src/InlineTest.Tests/SubClassTests.cs
@@ -0,0 +1,47 @@
+namespace Sungaila.InlineTest.Tests
+{
+ public class SubClassTests
+ {
+ [AreEqual(Expected = 0.0d)]
+ public double GetRealInstance() => 0.0d;
+
+ [AreEqual(Expected = 0.0d)]
+ public static double GetRealStatic() => 0.0d;
+
+ public class SubClassDepth1
+ {
+ [AreEqual(Expected = 0.0d)]
+ public double GetRealInstance() => 0.0d;
+
+ [AreEqual(Expected = 0.0d)]
+ public static double GetRealStatic() => 0.0d;
+
+ public class SubClassDepth2
+ {
+ [AreEqual(Expected = 0.0d)]
+ public double GetRealInstance() => 0.0d;
+
+ [AreEqual(Expected = 0.0d)]
+ public static double GetRealStatic() => 0.0d;
+
+ public class SubClassDepth3
+ {
+ [AreEqual(Expected = 0.0d)]
+ public double GetRealInstance() => 0.0d;
+
+ [AreEqual(Expected = 0.0d)]
+ public static double GetRealStatic() => 0.0d;
+
+ public class SubClassDepth4
+ {
+ [AreEqual(Expected = 0.0d)]
+ public double GetRealInstance() => 0.0d;
+
+ [AreEqual(Expected = 0.0d)]
+ public static double GetRealStatic() => 0.0d;
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/InlineTest.Tests/ThrowsExceptionTests.cs b/src/InlineTest.Tests/ThrowsExceptionTests.cs
index 7709340..6fb1ea7 100644
--- a/src/InlineTest.Tests/ThrowsExceptionTests.cs
+++ b/src/InlineTest.Tests/ThrowsExceptionTests.cs
@@ -18,7 +18,7 @@ public class ThrowsExceptionTests
}
[ThrowsException("Null")]
- [ThrowsException]
+ [ThrowsException("Something")]
public object? Throw(string input)
{
if (input == "Null")
@@ -28,7 +28,7 @@ public class ThrowsExceptionTests
}
[ThrowsException("Null")]
- [ThrowsException]
+ [ThrowsException("Something")]
public object? Throw2(string input)
{
if (input == "Null")
diff --git a/src/InlineTest/AnalyzerReleases.Shipped.md b/src/InlineTest/AnalyzerReleases.Shipped.md
new file mode 100644
index 0000000..4d86cdf
--- /dev/null
+++ b/src/InlineTest/AnalyzerReleases.Shipped.md
@@ -0,0 +1,15 @@
+; Shipped analyzer releases
+; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
+## Release 1.0.0
+
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|-------
+IT0000 | Method | Error | InlineTestAnalyzer, [Documentation](https://github.com/sungaila/InlineTest)
+IT0001 | Method | Error | InlineTestAnalyzer, [Documentation](https://github.com/sungaila/InlineTest)
+IT0002 | Method | Error | InlineTestAnalyzer, [Documentation](https://github.com/sungaila/InlineTest)
+IT0003 | Method | Warning | InlineTestAnalyzer, [Documentation](https://github.com/sungaila/InlineTest)
+IT0004 | Style | Warning | InlineTestAnalyzer, [Documentation](https://github.com/sungaila/InlineTest)
+IT0005 | Style | Warning | InlineTestAnalyzer, [Documentation](https://github.com/sungaila/InlineTest)
\ No newline at end of file
diff --git a/src/InlineTest/AnalyzerReleases.Unshipped.md b/src/InlineTest/AnalyzerReleases.Unshipped.md
new file mode 100644
index 0000000..b1b99aa
--- /dev/null
+++ b/src/InlineTest/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,3 @@
+; Unshipped analyzer release
+; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
diff --git a/src/InlineTest/CodeFixProviders/AreEqualNoExpectedValueCodeFixProvider.cs b/src/InlineTest/CodeFixProviders/AreEqualNoExpectedValueCodeFixProvider.cs
new file mode 100644
index 0000000..5b57134
--- /dev/null
+++ b/src/InlineTest/CodeFixProviders/AreEqualNoExpectedValueCodeFixProvider.cs
@@ -0,0 +1,68 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Sungaila.InlineTest.CodeFixProviders
+{
+ [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AreEqualNoExpectedValueCodeFixProvider)), Shared]
+ internal class AreEqualNoExpectedValueCodeFixProvider : CodeFixProvider
+ {
+ public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(InlineTestAnalyzer.AreEqualNoExpectedValueId);
+
+ public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
+
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+#if FALSE && DEBUG
+ if (!System.Diagnostics.Debugger.IsAttached)
+ {
+ System.Diagnostics.Debugger.Launch();
+ }
+#endif
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+
+ var diagnostic = context.Diagnostics.Single();
+ var diagnosticSpan = diagnostic.Location.SourceSpan;
+ var declaration = root!.FindToken(diagnosticSpan.Start).Parent!.FirstAncestorOrSelf();
+
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ "Add Expected parameter",
+ c => AddExpectedParameterAsync(context.Document, declaration!, c),
+ "Add Expected parameter"
+ ),
+ diagnostic);
+ }
+
+ private async Task AddExpectedParameterAsync(Document document, AttributeSyntax attributeSyntax, CancellationToken cancellationToken)
+ {
+ var newArgument = SyntaxFactory.AttributeArgument(
+ SyntaxFactory.NameEquals(nameof(AreEqualAttribute.Expected)),
+ null,
+ SyntaxFactory.LiteralExpression(SyntaxKind.DefaultLiteralExpression)
+ );
+
+ var oldRoot = await document.GetSyntaxRootAsync(cancellationToken);
+ SyntaxNode newRoot;
+
+ if (attributeSyntax.ArgumentList == null)
+ {
+ newRoot = oldRoot!.ReplaceNode(attributeSyntax,
+ attributeSyntax.WithArgumentList(SyntaxFactory.AttributeArgumentList(SyntaxFactory.SeparatedList(new[] { newArgument }))));
+ }
+ else
+ {
+ newRoot = oldRoot!.ReplaceNode(attributeSyntax, attributeSyntax.AddArgumentListArguments(newArgument));
+ }
+
+ return document.WithSyntaxRoot(newRoot);
+ }
+ }
+}
diff --git a/src/InlineTest/CodeFixProviders/AreNotEqualNoNotExpectedValueCodeFixProvider.cs b/src/InlineTest/CodeFixProviders/AreNotEqualNoNotExpectedValueCodeFixProvider.cs
new file mode 100644
index 0000000..29620ab
--- /dev/null
+++ b/src/InlineTest/CodeFixProviders/AreNotEqualNoNotExpectedValueCodeFixProvider.cs
@@ -0,0 +1,68 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Sungaila.InlineTest.CodeFixProviders
+{
+ [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AreEqualNoExpectedValueCodeFixProvider)), Shared]
+ internal class AreNotEqualNoNotExpectedValueCodeFixProvider : CodeFixProvider
+ {
+ public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(InlineTestAnalyzer.AreNotEqualNoNotExpectedValueId);
+
+ public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
+
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+#if FALSE && DEBUG
+ if (!System.Diagnostics.Debugger.IsAttached)
+ {
+ System.Diagnostics.Debugger.Launch();
+ }
+#endif
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+
+ var diagnostic = context.Diagnostics.Single();
+ var diagnosticSpan = diagnostic.Location.SourceSpan;
+ var declaration = root!.FindToken(diagnosticSpan.Start).Parent!.FirstAncestorOrSelf();
+
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ "Add NotExpected parameter",
+ c => AddExpectedParameterAsync(context.Document, declaration!, c),
+ "Add NotExpected parameter"
+ ),
+ diagnostic);
+ }
+
+ private async Task AddExpectedParameterAsync(Document document, AttributeSyntax attributeSyntax, CancellationToken cancellationToken)
+ {
+ var newArgument = SyntaxFactory.AttributeArgument(
+ SyntaxFactory.NameEquals(nameof(AreNotEqualAttribute.NotExpected)),
+ null,
+ SyntaxFactory.LiteralExpression(SyntaxKind.DefaultLiteralExpression)
+ );
+
+ var oldRoot = await document.GetSyntaxRootAsync(cancellationToken);
+ SyntaxNode newRoot;
+
+ if (attributeSyntax.ArgumentList == null)
+ {
+ newRoot = oldRoot!.ReplaceNode(attributeSyntax,
+ attributeSyntax.WithArgumentList(SyntaxFactory.AttributeArgumentList(SyntaxFactory.SeparatedList(new[] { newArgument }))));
+ }
+ else
+ {
+ newRoot = oldRoot!.ReplaceNode(attributeSyntax, attributeSyntax.AddArgumentListArguments(newArgument));
+ }
+
+ return document.WithSyntaxRoot(newRoot);
+ }
+ }
+}
diff --git a/src/InlineTest/GeneratorContextHelpers.cs b/src/InlineTest/GeneratorContextHelpers.cs
index 97a1941..4ee715e 100644
--- a/src/InlineTest/GeneratorContextHelpers.cs
+++ b/src/InlineTest/GeneratorContextHelpers.cs
@@ -4,8 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Reflection.Metadata;
-using System.Threading.Tasks;
namespace Sungaila.InlineTest
{
@@ -20,6 +18,24 @@ public static string GetFullyQualifiedMetadataName(this ISymbol namedTypeSymbol)
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters));
}
+ public static string GetFullyQualifiedMetadataNameWithoutGlobal(this ISymbol namedTypeSymbol)
+ {
+ return namedTypeSymbol.ToDisplayString(new SymbolDisplayFormat(
+ globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted,
+ typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
+ memberOptions: SymbolDisplayMemberOptions.IncludeContainingType,
+ genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters));
+ }
+
+ public static string GetFullyQualifiedMetadataNameWithoutTypeParameters(this ISymbol namedTypeSymbol)
+ {
+ return namedTypeSymbol.ToDisplayString(new SymbolDisplayFormat(
+ globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted,
+ typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
+ memberOptions: SymbolDisplayMemberOptions.IncludeContainingType,
+ genericsOptions: SymbolDisplayGenericsOptions.None));
+ }
+
public static Microsoft.CodeAnalysis.TypeInfo GetTypeInfo(this SyntaxNode source, GeneratorExecutionContext context)
{
return context.Compilation.GetSemanticModel(source.SyntaxTree, true).GetTypeInfo(source, context.CancellationToken);
@@ -53,10 +69,16 @@ public static string InvokeMethod(this MethodDeclarationSyntax methodDef, Genera
return $"{(isAsyncMethod ? "await " : string.Empty)}{methodDef.GetDeclaredSymbol(context).GetFullyQualifiedMetadataName()}({paramWithoutTypeList}){(withSemicolon ? ";" : string.Empty)}";
}
- if (methodDef.FirstAncestorOrSelf() is not ClassDeclarationSyntax classDef)
+ TypeDeclarationSyntax? classOrStructDef;
+
+ if (methodDef.FirstAncestorOrSelf() is ClassDeclarationSyntax classDef)
+ classOrStructDef = classDef;
+ else if (methodDef.FirstAncestorOrSelf() is StructDeclarationSyntax structDef)
+ classOrStructDef = structDef;
+ else
throw new InvalidOperationException();
- return $"{(isAsyncMethod ? "await " : string.Empty)}new {classDef.GetDeclaredSymbol(context).GetFullyQualifiedMetadataName()}().{methodDef.Identifier.ValueText}({paramWithoutTypeList}){(withSemicolon ? ";" : string.Empty)}";
+ return $"{(isAsyncMethod ? "await " : string.Empty)}new {classOrStructDef.GetDeclaredSymbol(context).GetFullyQualifiedMetadataName()}().{methodDef.Identifier.ValueText}({paramWithoutTypeList}){(withSemicolon ? ";" : string.Empty)}";
}
public static string InvokeMethodWithResult(this MethodDeclarationSyntax methodDef, GeneratorExecutionContext context)
diff --git a/src/InlineTest/InlineTest.csproj b/src/InlineTest/InlineTest.csproj
index b8d5f76..70c51e2 100644
--- a/src/InlineTest/InlineTest.csproj
+++ b/src/InlineTest/InlineTest.csproj
@@ -14,8 +14,8 @@
- 0.9.0
- preview
+ 1.0.0
+
David Sungaila
false
MIT
@@ -23,7 +23,9 @@
https://github.com/sungaila/InlineTest
https://raw.githubusercontent.com/sungaila/InlineTest/master/etc/Icon_128.png
A C# source generator for quick creation of simple unit tests.
- Initial release
+ - Added support for nested classes.
+- Added support for structs.
+- Added analyzers and code fixes for incorrectly set attributes.
Roslyn CodeAnalysis CSharp C# Analyzers DotNetAnalyzers SourceGenerator ISourceGenerator UnitTest MSTest testing TestClass TestMethod DataRow
https://github.com/sungaila/InlineTest.git
git
diff --git a/src/InlineTest/InlineTestAnalyzer.cs b/src/InlineTest/InlineTestAnalyzer.cs
new file mode 100644
index 0000000..87301e3
--- /dev/null
+++ b/src/InlineTest/InlineTestAnalyzer.cs
@@ -0,0 +1,245 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using System.Collections.Immutable;
+using System.Linq;
+
+namespace Sungaila.InlineTest
+{
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ internal class InlineTestAnalyzer : DiagnosticAnalyzer
+ {
+ public const string ParameterCountExceededId = "IT0000";
+
+ private static readonly DiagnosticDescriptor ParameterCountExceededRule = new(
+ ParameterCountExceededId,
+ "Method exceeds 15 parameters limit for inline tests",
+ "Method exceeds 15 parameters limit for inline tests",
+ "Method",
+ DiagnosticSeverity.Error,
+ true,
+ "A method cannot have more than 15 parameters for inline tests. This is a technical limitation because DataRowAttribute supports 16 parameters only (one parameter is reserved for the expected value).",
+ "https://github.com/sungaila/InlineTest"
+ );
+
+ public const string ParameterCountMismatchId = "IT0001";
+
+ private static readonly DiagnosticDescriptor ParameterCountMismatchRule = new(
+ ParameterCountMismatchId,
+ "Method parameter count mismatch",
+ "Method parameter count mismatch",
+ "Method",
+ DiagnosticSeverity.Error,
+ true,
+ "The method parameter count and the inline test parameter count must match. Please note that optional parameters do not count.",
+ "https://github.com/sungaila/InlineTest"
+ );
+
+ public const string ParameterTypesMismatchId = "IT0002";
+
+ private static readonly DiagnosticDescriptor ParameterTypesMismatchRule = new(
+ ParameterTypesMismatchId,
+ "Method parameter types mismatch",
+ "Method parameter types mismatch",
+ "Method",
+ DiagnosticSeverity.Error,
+ true,
+ "The method parameter types and the inline test parameter types must match.",
+ "https://github.com/sungaila/InlineTest"
+ );
+
+ public const string AreEqualNoReturnTypeId = "IT0003";
+
+ private static readonly DiagnosticDescriptor AreEqualNoReturnTypeRule = new(
+ AreEqualNoReturnTypeId,
+ "Method has no return type but compares values",
+ "Method has no return type but compares values",
+ "Method",
+ DiagnosticSeverity.Warning,
+ true,
+ $"Inline tests cannot compare expected and actual values if a method has no return type.",
+ "https://github.com/sungaila/InlineTest"
+ );
+
+ public const string AreEqualNoExpectedValueId = "IT0004";
+
+ private static readonly DiagnosticDescriptor AreEqualNoExpectedValueRule = new(
+ AreEqualNoExpectedValueId,
+ $"{nameof(AreEqualAttribute)} missing {nameof(AreEqualAttribute.Expected)} property",
+ $"{nameof(AreEqualAttribute)} missing {nameof(AreEqualAttribute.Expected)} property",
+ "Style",
+ DiagnosticSeverity.Warning,
+ true,
+ $"{nameof(AreEqualAttribute)} should set the {nameof(AreEqualAttribute.Expected)} property explicitly. Otherwise default is used as fallback.",
+ "https://github.com/sungaila/InlineTest"
+ );
+
+ public const string AreNotEqualNoNotExpectedValueId = "IT0005";
+
+ private static readonly DiagnosticDescriptor AreNotEqualNoNotExpectedValueRule = new(
+ AreNotEqualNoNotExpectedValueId,
+ $"{nameof(AreNotEqualAttribute)} missing {nameof(AreNotEqualAttribute.NotExpected)} property",
+ $"{nameof(AreNotEqualAttribute)} missing {nameof(AreNotEqualAttribute.NotExpected)} property",
+ "Style",
+ DiagnosticSeverity.Warning,
+ true,
+ $"{nameof(AreNotEqualAttribute)} should set the {nameof(AreNotEqualAttribute.NotExpected)} property explicitly. Otherwise default is used as fallback.",
+ "https://github.com/sungaila/InlineTest"
+ );
+
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(
+ ParameterCountExceededRule,
+ ParameterCountMismatchRule,
+ ParameterTypesMismatchRule,
+ AreEqualNoReturnTypeRule,
+ AreEqualNoExpectedValueRule,
+ AreNotEqualNoNotExpectedValueRule);
+
+ public override void Initialize(AnalysisContext context)
+ {
+#if FALSE && DEBUG
+ if (!System.Diagnostics.Debugger.IsAttached)
+ {
+ System.Diagnostics.Debugger.Launch();
+ }
+#endif
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+
+ context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method);
+ }
+
+ private void AnalyzeMethod(SymbolAnalysisContext context)
+ {
+ if (context.Symbol is not IMethodSymbol methodSymbol)
+ return;
+
+ if (methodSymbol.Parameters.Length > 15)
+ {
+ context.ReportDiagnostic(
+ Diagnostic.Create(
+ ParameterCountExceededRule,
+ methodSymbol.Locations.First(l => l.IsInSource),
+ methodSymbol.Name));
+ }
+
+ foreach (var attr in methodSymbol.GetAttributes().Where(a => a.AttributeClass?.BaseType?.GetFullyQualifiedMetadataNameWithoutTypeParameters() == typeof(InlineTestAttributeBase).FullName))
+ {
+ var attrParamCount = attr.ConstructorArguments.FirstOrDefault().Values.Count();
+
+ if (attrParamCount < methodSymbol.Parameters.Count(p => !p.IsOptional) ||
+ attrParamCount > methodSymbol.Parameters.Length)
+ {
+ context.ReportDiagnostic(
+ Diagnostic.Create(
+ ParameterCountMismatchRule,
+ attr.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation(),
+ methodSymbol.Name));
+ }
+
+ for (int i = 0; i < methodSymbol.Parameters.Length && i < attrParamCount; i++)
+ {
+ // TODO
+ }
+ }
+
+ foreach (var areEqualAttr in methodSymbol.GetAttributes().Where(a => a.AttributeClass?.GetFullyQualifiedMetadataNameWithoutTypeParameters() == typeof(AreEqualAttribute).FullName))
+ {
+ if (!areEqualAttr.NamedArguments.Any(a => a.Key == nameof(AreEqualAttribute.Expected)))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(AreEqualNoExpectedValueRule,
+ areEqualAttr.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation(),
+ methodSymbol.Name));
+ }
+
+ if (methodSymbol.ReturnsVoid)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(AreEqualNoReturnTypeRule,
+ areEqualAttr.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation(),
+ methodSymbol.Name));
+ }
+ }
+
+ foreach (var areNotEqualAttr in methodSymbol.GetAttributes().Where(a => a.AttributeClass?.GetFullyQualifiedMetadataNameWithoutTypeParameters() == typeof(AreNotEqualAttribute).FullName))
+ {
+ if (!areNotEqualAttr.NamedArguments.Any(a => a.Key == nameof(AreNotEqualAttribute.NotExpected)))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(AreNotEqualNoNotExpectedValueRule,
+ areNotEqualAttr.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation(),
+ methodSymbol.Name));
+ }
+
+ if (methodSymbol.ReturnsVoid)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(AreEqualNoReturnTypeRule,
+ areNotEqualAttr.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation(),
+ methodSymbol.Name));
+ }
+ }
+
+ foreach (var isTrueAttr in methodSymbol.GetAttributes().Where(a => a.AttributeClass?.GetFullyQualifiedMetadataNameWithoutTypeParameters() == typeof(IsTrueAttribute).FullName))
+ {
+ if (methodSymbol.ReturnsVoid)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(AreEqualNoReturnTypeRule,
+ isTrueAttr.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation(),
+ methodSymbol.Name));
+ }
+ }
+
+ foreach (var isFalseAttr in methodSymbol.GetAttributes().Where(a => a.AttributeClass?.GetFullyQualifiedMetadataNameWithoutTypeParameters() == typeof(IsFalseAttribute).FullName))
+ {
+ if (methodSymbol.ReturnsVoid)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(AreEqualNoReturnTypeRule,
+ isFalseAttr.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation(),
+ methodSymbol.Name));
+ }
+ }
+
+ foreach (var isNullAttr in methodSymbol.GetAttributes().Where(a => a.AttributeClass?.GetFullyQualifiedMetadataNameWithoutTypeParameters() == typeof(IsNullAttribute).FullName))
+ {
+ if (methodSymbol.ReturnsVoid)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(AreEqualNoReturnTypeRule,
+ isNullAttr.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation(),
+ methodSymbol.Name));
+ }
+ }
+
+ foreach (var isNotNullAttr in methodSymbol.GetAttributes().Where(a => a.AttributeClass?.GetFullyQualifiedMetadataNameWithoutTypeParameters() == typeof(IsNotNullAttribute).FullName))
+ {
+ if (methodSymbol.ReturnsVoid)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(AreEqualNoReturnTypeRule,
+ isNotNullAttr.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation(),
+ methodSymbol.Name));
+ }
+ }
+
+ foreach (var isInstanceOfTypeAttr in methodSymbol.GetAttributes().Where(a => $"{a.AttributeClass?.GetFullyQualifiedMetadataNameWithoutTypeParameters()}`1" == typeof(IsInstanceOfTypeAttribute<>).FullName))
+ {
+ if (methodSymbol.ReturnsVoid)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(AreEqualNoReturnTypeRule,
+ isInstanceOfTypeAttr.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation(),
+ methodSymbol.Name));
+ }
+ }
+
+ foreach (var isNotInstanceOfTypeAttr in methodSymbol.GetAttributes().Where(a => $"{a.AttributeClass?.GetFullyQualifiedMetadataNameWithoutTypeParameters()}`1" == typeof(IsNotInstanceOfTypeAttribute<>).FullName))
+ {
+ if (methodSymbol.ReturnsVoid)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(AreEqualNoReturnTypeRule,
+ isNotInstanceOfTypeAttr.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation(),
+ methodSymbol.Name));
+ }
+ }
+
+ foreach (var throwsExceptionAttr in methodSymbol.GetAttributes().Where(a => $"{a.AttributeClass?.GetFullyQualifiedMetadataNameWithoutTypeParameters()}`1" == typeof(ThrowsExceptionAttribute<>).FullName))
+ {
+ // NOP
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/InlineTest/InlineTestGenerator.cs b/src/InlineTest/InlineTestGenerator.cs
index bbf038b..79787dc 100644
--- a/src/InlineTest/InlineTestGenerator.cs
+++ b/src/InlineTest/InlineTestGenerator.cs
@@ -3,14 +3,13 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
-using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
namespace Sungaila.InlineTest
{
- [Generator]
+ [Generator(LanguageNames.CSharp)]
internal class InlineTestGenerator : ISourceGenerator
{
///
@@ -43,6 +42,12 @@ public void Execute(GeneratorExecutionContext context)
.Distinct()
.ToList();
+ var structDefs = attributeDefs
+ .Select(a => a.FirstAncestorOrSelf())
+ .OfType()
+ .Distinct()
+ .ToList();
+
var methodDefs = attributeDefs
.Select(a => a.FirstAncestorOrSelf())
.OfType()
@@ -58,25 +63,25 @@ public void Execute(GeneratorExecutionContext context)
sb.AppendLine($"namespace Sungaila.InlineTest.Generated");
sb.Append("{");
- foreach (var classDef in classDefs)
+ foreach (var classOrStructDef in classDefs.Cast().Union(structDefs))
{
- if (classDef.FirstAncestorOrSelf() is not NamespaceDeclarationSyntax namespaceDef)
+ if (classOrStructDef.FirstAncestorOrSelf() is not NamespaceDeclarationSyntax namespaceDef)
continue;
- var classTypeInfo = classDef.GetDeclaredSymbol(context);
+ var classOrStructTypeInfo = classOrStructDef.GetDeclaredSymbol(context);
sb.AppendLine();
sb.AppendLine("\t/// ");
- sb.AppendLine($"\t/// Test class for .");
+ sb.AppendLine($"\t/// Test class for .");
sb.AppendLine("\t/// ");
sb.AppendLine($"\t[GeneratedCode(\"{typeof(InlineTestGenerator).Assembly.GetName().Name}\", \"{typeof(InlineTestGenerator).Assembly.GetCustomAttribute()?.InformationalVersion ?? typeof(InlineTestGenerator).Assembly.GetName().Version.ToString()}\")]");
sb.AppendLine("\t[TestClass]");
- sb.AppendLine($"\tpublic partial class {classDef.Identifier.ValueText}Tests");
+ sb.AppendLine($"\tpublic partial class {classOrStructDef.GetDeclaredSymbol(context).GetFullyQualifiedMetadataNameWithoutGlobal().Replace(".", "_")}");
sb.Append("\t{");
- foreach (var methodDef in methodDefs.Where(m => m.FirstAncestorOrSelf((c) => c == classDef) != null))
+ foreach (var methodDef in methodDefs.Where(m => m.FirstAncestorOrSelf() == classOrStructDef || m.FirstAncestorOrSelf() == classOrStructDef))
{
- var allAttributes = attributeDefs.Where(a => a.FirstAncestorOrSelf((c) => c == methodDef) != null).ToList();
+ var allAttributes = attributeDefs.Where(a => a.FirstAncestorOrSelf() == methodDef).ToList();
var areEqualAttributes = allAttributes.Where(a => a.ConversionExists(context, areEqualSymbol)).ToList();
var areNotEqualAttributes = allAttributes.Where(a => a.ConversionExists(context, areNotEqualSymbol)).ToList();
var isTrueAttributes = allAttributes.Where(a => a.ConversionExists(context, isTrueSymbol)).ToList();