From 6fabf1d25e9c20fd241e4c783c9d7a9e8d6fbf4b Mon Sep 17 00:00:00 2001 From: David Sungaila Date: Sat, 11 Mar 2023 00:35:16 +0100 Subject: [PATCH] Fixes and inprovements --- README.md | 9 +- src/InlineTest.Tests/InlineTest.Tests.csproj | 3 +- src/InlineTest.Tests/MethodOverloadTests.cs | 17 ++ src/InlineTest.Tests/ReadmeExample.cs | 2 +- src/InlineTest.Tests/StructTests.cs | 51 ++++ src/InlineTest.Tests/SubClassTests.cs | 47 ++++ src/InlineTest.Tests/ThrowsExceptionTests.cs | 4 +- src/InlineTest/AnalyzerReleases.Shipped.md | 15 ++ src/InlineTest/AnalyzerReleases.Unshipped.md | 3 + .../AreEqualNoExpectedValueCodeFixProvider.cs | 68 +++++ ...tEqualNoNotExpectedValueCodeFixProvider.cs | 68 +++++ src/InlineTest/GeneratorContextHelpers.cs | 30 ++- src/InlineTest/InlineTest.csproj | 8 +- src/InlineTest/InlineTestAnalyzer.cs | 245 ++++++++++++++++++ src/InlineTest/InlineTestGenerator.cs | 23 +- 15 files changed, 569 insertions(+), 24 deletions(-) create mode 100644 src/InlineTest.Tests/MethodOverloadTests.cs create mode 100644 src/InlineTest.Tests/StructTests.cs create mode 100644 src/InlineTest.Tests/SubClassTests.cs create mode 100644 src/InlineTest/AnalyzerReleases.Shipped.md create mode 100644 src/InlineTest/AnalyzerReleases.Unshipped.md create mode 100644 src/InlineTest/CodeFixProviders/AreEqualNoExpectedValueCodeFixProvider.cs create mode 100644 src/InlineTest/CodeFixProviders/AreNotEqualNoNotExpectedValueCodeFixProvider.cs create mode 100644 src/InlineTest/InlineTestAnalyzer.cs 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();