From 75f409e6ab2071bb813e45bce1182bfbbd73b653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sat, 4 Apr 2026 20:22:55 +0200 Subject: [PATCH 1/3] feat: add initial analyzer that detects Moq --- .github/workflows/build.yml | 28 +--- .github/workflows/ci.yml | 28 +--- Directory.Packages.props | 15 ++- Mockolate.Migration.slnx | 4 +- Pipeline/Build.ApiChecks.cs | 28 ---- Pipeline/Build.cs | 2 +- Source/Directory.Build.props | 8 +- .../AssertionCodeFixProvider.cs | 51 ++++++++ ...late.Migration.Analyzers.CodeFixers.csproj | 32 +++++ .../MoqCodeFixProvider.cs | 35 +++++ .../AnalyzerReleases.Shipped.md | 7 + .../AnalyzerReleases.Unshipped.md | 4 + .../Common/TypeExtensions.cs | 36 ++++++ .../Mockolate.Migration.Analyzers.csproj | 42 ++++++ .../MoqAnalyzer.cs | 73 +++++++++++ .../Resources.Designer.cs | 98 ++++++++++++++ .../Resources.resx | 40 ++++++ Source/Mockolate.Migration.Analyzers/Rules.cs | 32 +++++ Tests/Directory.Build.props | 6 +- .../ApiAcceptance.cs | 28 ---- .../ApiApprovalTests.cs | 32 ----- .../Expected/Mockolate.Migration_net10.0.txt | 2 - .../Expected/Mockolate.Migration_net8.0.txt | 2 - .../Mockolate.Migration_netstandard2.0.txt | 2 - Tests/Mockolate.Migration.Api.Tests/Helper.cs | 68 ---------- .../Mockolate.Migration.Api.Tests.csproj | 18 --- .../Examples.cs | 21 +++ .../IChocolateDispenser.cs | 11 ++ .../Mockolate.Migration.Example.Tests.csproj | 27 ++++ .../Usings.cs | 0 Tests/Mockolate.Migration.Tests/DummyTests.cs | 15 --- .../Mockolate.Migration.Tests.csproj | 10 ++ .../MoqAnalyzerTests.cs | 43 +++++++ .../Verifiers/CSharpAnalyzerVerifier.cs | 49 +++++++ .../Verifiers/CSharpAnalyzerVerifier`1.cs | 49 +++++++ .../Verifiers/CSharpCodeFixVerifier.cs | 51 ++++++++ .../Verifiers/CSharpCodeFixVerifier`2.cs | 121 ++++++++++++++++++ .../CSharpCodeRefactoringVerifier.cs | 49 +++++++ .../CSharpCodeRefactoringVerifier`1.cs | 32 +++++ .../Verifiers/CSharpVerifierHelper.cs | 35 +++++ 40 files changed, 973 insertions(+), 261 deletions(-) delete mode 100644 Pipeline/Build.ApiChecks.cs create mode 100644 Source/Mockolate.Migration.Analyzers.CodeFixers/AssertionCodeFixProvider.cs create mode 100644 Source/Mockolate.Migration.Analyzers.CodeFixers/Mockolate.Migration.Analyzers.CodeFixers.csproj create mode 100644 Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs create mode 100644 Source/Mockolate.Migration.Analyzers/AnalyzerReleases.Shipped.md create mode 100644 Source/Mockolate.Migration.Analyzers/AnalyzerReleases.Unshipped.md create mode 100644 Source/Mockolate.Migration.Analyzers/Common/TypeExtensions.cs create mode 100644 Source/Mockolate.Migration.Analyzers/Mockolate.Migration.Analyzers.csproj create mode 100644 Source/Mockolate.Migration.Analyzers/MoqAnalyzer.cs create mode 100644 Source/Mockolate.Migration.Analyzers/Resources.Designer.cs create mode 100644 Source/Mockolate.Migration.Analyzers/Resources.resx create mode 100644 Source/Mockolate.Migration.Analyzers/Rules.cs delete mode 100644 Tests/Mockolate.Migration.Api.Tests/ApiAcceptance.cs delete mode 100644 Tests/Mockolate.Migration.Api.Tests/ApiApprovalTests.cs delete mode 100644 Tests/Mockolate.Migration.Api.Tests/Expected/Mockolate.Migration_net10.0.txt delete mode 100644 Tests/Mockolate.Migration.Api.Tests/Expected/Mockolate.Migration_net8.0.txt delete mode 100644 Tests/Mockolate.Migration.Api.Tests/Expected/Mockolate.Migration_netstandard2.0.txt delete mode 100644 Tests/Mockolate.Migration.Api.Tests/Helper.cs delete mode 100644 Tests/Mockolate.Migration.Api.Tests/Mockolate.Migration.Api.Tests.csproj create mode 100644 Tests/Mockolate.Migration.Example.Tests/Examples.cs create mode 100644 Tests/Mockolate.Migration.Example.Tests/IChocolateDispenser.cs create mode 100644 Tests/Mockolate.Migration.Example.Tests/Mockolate.Migration.Example.Tests.csproj rename Tests/{Mockolate.Migration.Api.Tests => Mockolate.Migration.Example.Tests}/Usings.cs (100%) delete mode 100644 Tests/Mockolate.Migration.Tests/DummyTests.cs create mode 100644 Tests/Mockolate.Migration.Tests/MoqAnalyzerTests.cs create mode 100644 Tests/Mockolate.Migration.Tests/Verifiers/CSharpAnalyzerVerifier.cs create mode 100644 Tests/Mockolate.Migration.Tests/Verifiers/CSharpAnalyzerVerifier`1.cs create mode 100644 Tests/Mockolate.Migration.Tests/Verifiers/CSharpCodeFixVerifier.cs create mode 100644 Tests/Mockolate.Migration.Tests/Verifiers/CSharpCodeFixVerifier`2.cs create mode 100644 Tests/Mockolate.Migration.Tests/Verifiers/CSharpCodeRefactoringVerifier.cs create mode 100644 Tests/Mockolate.Migration.Tests/Verifiers/CSharpCodeRefactoringVerifier`1.cs create mode 100644 Tests/Mockolate.Migration.Tests/Verifiers/CSharpVerifierHelper.cs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 07a055e..9647fe7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,32 +37,6 @@ jobs: ./Artifacts/* ./TestResults/*.trx - api-tests: - name: "API tests" - runs-on: ubuntu-latest - env: - DOTNET_NOLOGO: true - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - name: Setup .NET SDKs - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 8.0.x - 9.0.x - - name: API checks - run: ./build.sh ApiChecks - - name: Upload artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: API-tests - path: | - ./Artifacts/* - ./TestResults/*.trx - benchmarks: name: "Benchmarks" runs-on: ubuntu-latest @@ -105,7 +79,7 @@ jobs: publish-test-results: name: "Publish Tests Results" - needs: [ api-tests, unit-tests ] + needs: [ unit-tests ] runs-on: ubuntu-latest permissions: checks: write diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c62e27..6a525ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,32 +37,6 @@ jobs: ./Artifacts/* ./TestResults/*.trx - api-tests: - name: "API tests" - runs-on: ubuntu-latest - env: - DOTNET_NOLOGO: true - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - name: Setup .NET SDKs - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 8.0.x - 9.0.x - - name: API checks - run: ./build.sh ApiChecks - - name: Upload artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: API-tests - path: | - ./Artifacts/* - ./TestResults/*.trx - benchmarks: name: "Benchmarks" runs-on: ubuntu-latest @@ -111,7 +85,7 @@ jobs: publish-test-results: name: "Publish Tests Results" - needs: [ api-tests, unit-tests ] + needs: [ unit-tests ] runs-on: ubuntu-latest permissions: checks: write diff --git a/Directory.Packages.props b/Directory.Packages.props index 18dc422..20746ca 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,6 +14,17 @@ + + + + + + + + + + + @@ -23,8 +34,10 @@ - + + + diff --git a/Mockolate.Migration.slnx b/Mockolate.Migration.slnx index 68f7c73..3118d4d 100644 --- a/Mockolate.Migration.slnx +++ b/Mockolate.Migration.slnx @@ -6,7 +6,7 @@ - + @@ -35,5 +35,7 @@ + + diff --git a/Pipeline/Build.ApiChecks.cs b/Pipeline/Build.ApiChecks.cs deleted file mode 100644 index 7899bd3..0000000 --- a/Pipeline/Build.ApiChecks.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Nuke.Common; -using Nuke.Common.ProjectModel; -using Nuke.Common.Tooling; -using Nuke.Common.Tools.DotNet; -using static Nuke.Common.Tools.DotNet.DotNetTasks; - -// ReSharper disable AllUnderscoreLocalParameterName - -namespace Build; - -partial class Build -{ - Target ApiChecks => _ => _ - .DependsOn(Compile) - .Executes(() => - { - Project project = Solution.Tests.Mockolate_Migration_Api_Tests; - - DotNetTest(s => s - .SetConfiguration(Configuration == Configuration.Debug ? "Debug" : "Release") - .SetProcessEnvironmentVariable("DOTNET_CLI_UI_LANGUAGE", "en-US") - .EnableNoBuild() - .SetResultsDirectory(TestResultsDirectory) - .CombineWith(cc => cc - .SetProjectFile(project) - .AddLoggers($"trx;LogFileName={project.Name}.trx")), completeOnFailure: true); - }); -} diff --git a/Pipeline/Build.cs b/Pipeline/Build.cs index ee7a477..7aa34f7 100644 --- a/Pipeline/Build.cs +++ b/Pipeline/Build.cs @@ -27,5 +27,5 @@ partial class Build : NukeBuild AbsolutePath TestResultsDirectory => RootDirectory / "TestResults"; GitHubActions GitHubActions => GitHubActions.Instance; - public static int Main() => Execute(x => x.ApiChecks, x => x.Benchmarks, x => x.CodeAnalysis); + public static int Main() => Execute(x => x.Benchmarks, x => x.CodeAnalysis); } diff --git a/Source/Directory.Build.props b/Source/Directory.Build.props index 2d29209..353a7ce 100644 --- a/Source/Directory.Build.props +++ b/Source/Directory.Build.props @@ -4,9 +4,9 @@ Condition="Exists('$(MSBuildThisFileDirectory)/../Directory.Build.props')"/> - aweXpect - Template for extension projects for aweXpect. - Copyright (c) 2025 - $([System.DateTime]::Now.ToString('yyyy')) Valentin Breuß + Mockolate + Migration helpers from other mocking libraries. + Copyright (c) 2026 - $([System.DateTime]::Now.ToString('yyyy')) Valentin Breuß https://github.com/aweXpect/Mockolate.Migration.git git MIT @@ -15,7 +15,7 @@ - net10.0;net8.0;netstandard2.0 + netstandard2.0 diff --git a/Source/Mockolate.Migration.Analyzers.CodeFixers/AssertionCodeFixProvider.cs b/Source/Mockolate.Migration.Analyzers.CodeFixers/AssertionCodeFixProvider.cs new file mode 100644 index 0000000..64cb2d6 --- /dev/null +++ b/Source/Mockolate.Migration.Analyzers.CodeFixers/AssertionCodeFixProvider.cs @@ -0,0 +1,51 @@ +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Mockolate.Migration.Analyzers; + +/// +/// Base class for code fix provider that migrates assertions to aweXpect. +/// +public abstract class AssertionCodeFixProvider(DiagnosticDescriptor rule) : CodeFixProvider +{ + /// + public sealed override ImmutableArray FixableDiagnosticIds { get; } = [rule.Id,]; + + /// + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + /// + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + foreach (Diagnostic? diagnostic in context.Diagnostics) + { + TextSpan diagnosticSpan = diagnostic.Location.SourceSpan; + + SyntaxNode? root = + await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + if (root?.FindNode(diagnosticSpan) is ExpressionSyntax expressionSyntax + and (InvocationExpressionSyntax or ConditionalAccessExpressionSyntax or LambdaExpressionSyntax)) + { + context.RegisterCodeFix( + CodeAction.Create( + rule.Title.ToString(), + c => ConvertAssertionAsync(context, expressionSyntax, c), + rule.Title.ToString()), + diagnostic); + } + } + } + + /// + /// Converts the assertion. + /// + protected abstract Task ConvertAssertionAsync(CodeFixContext context, + ExpressionSyntax expressionSyntax, CancellationToken cancellationToken); +} diff --git a/Source/Mockolate.Migration.Analyzers.CodeFixers/Mockolate.Migration.Analyzers.CodeFixers.csproj b/Source/Mockolate.Migration.Analyzers.CodeFixers/Mockolate.Migration.Analyzers.CodeFixers.csproj new file mode 100644 index 0000000..cea8355 --- /dev/null +++ b/Source/Mockolate.Migration.Analyzers.CodeFixers/Mockolate.Migration.Analyzers.CodeFixers.csproj @@ -0,0 +1,32 @@ + + + + Code fix providers for Mockolate to migrate from other mocking libraries. + true + true + false + true + RS2003 + false + false + + + + Mockolate.Migration.Analyzers + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs b/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs new file mode 100644 index 0000000..b301136 --- /dev/null +++ b/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs @@ -0,0 +1,35 @@ +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +#pragma warning disable S1192 // String literals should not be duplicated +namespace Mockolate.Migration.Analyzers; + +/// +/// A code fix provider that migrates Moq to Mockolate. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MoqCodeFixProvider))] +[Shared] +public class MoqCodeFixProvider() : AssertionCodeFixProvider(Rules.MoqRule) +{ + /// + protected override async Task ConvertAssertionAsync(CodeFixContext context, + ExpressionSyntax expressionSyntax, CancellationToken cancellationToken) + { + Document? document = context.Document; + + SyntaxNode? root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + if (root is not CompilationUnitSyntax compilationUnit) + { + return document; + } + + return document; + } +} + +#pragma warning restore S1192 diff --git a/Source/Mockolate.Migration.Analyzers/AnalyzerReleases.Shipped.md b/Source/Mockolate.Migration.Analyzers/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000..2dbefb1 --- /dev/null +++ b/Source/Mockolate.Migration.Analyzers/AnalyzerReleases.Shipped.md @@ -0,0 +1,7 @@ +## Release 1.0 + +### New Rules + + Rule ID | Category | Severity | Notes +--------------|----------|----------|------------------------------------------- + MockolateM001 | Usage | Warning | Moq should be migrated. diff --git a/Source/Mockolate.Migration.Analyzers/AnalyzerReleases.Unshipped.md b/Source/Mockolate.Migration.Analyzers/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..fbed6a8 --- /dev/null +++ b/Source/Mockolate.Migration.Analyzers/AnalyzerReleases.Unshipped.md @@ -0,0 +1,4 @@ +### New Rules + + Rule ID | Category | Severity | Notes +---------|----------|----------|------- diff --git a/Source/Mockolate.Migration.Analyzers/Common/TypeExtensions.cs b/Source/Mockolate.Migration.Analyzers/Common/TypeExtensions.cs new file mode 100644 index 0000000..bde6e5a --- /dev/null +++ b/Source/Mockolate.Migration.Analyzers/Common/TypeExtensions.cs @@ -0,0 +1,36 @@ +using Microsoft.CodeAnalysis; + +namespace Mockolate.Migration.Analyzers.Common; + +internal static class TypeExtensions +{ + private static readonly SymbolDisplayFormat FullyQualifiedNonGenericWithGlobalPrefix = new( + SymbolDisplayGlobalNamespaceStyle.Included, + SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + SymbolDisplayGenericsOptions.None, + SymbolDisplayMemberOptions.IncludeContainingType, + SymbolDisplayDelegateStyle.NameAndSignature, + SymbolDisplayExtensionMethodStyle.Default, + SymbolDisplayParameterOptions.IncludeType, + SymbolDisplayPropertyStyle.NameOnly, + SymbolDisplayLocalOptions.IncludeType + ); + + private static readonly SymbolDisplayFormat? FullyQualifiedGenericWithGlobalPrefix = new( + SymbolDisplayGlobalNamespaceStyle.Included, + SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + SymbolDisplayGenericsOptions.None, + SymbolDisplayMemberOptions.IncludeContainingType, + SymbolDisplayDelegateStyle.NameAndSignature, + SymbolDisplayExtensionMethodStyle.Default, + SymbolDisplayParameterOptions.IncludeType, + SymbolDisplayPropertyStyle.NameOnly, + SymbolDisplayLocalOptions.IncludeType + ); + + public static string GloballyQualified(this ISymbol typeSymbol) => + typeSymbol.ToDisplayString(FullyQualifiedGenericWithGlobalPrefix); + + public static string GloballyQualifiedNonGeneric(this ISymbol typeSymbol) => + typeSymbol.ToDisplayString(FullyQualifiedNonGenericWithGlobalPrefix); +} diff --git a/Source/Mockolate.Migration.Analyzers/Mockolate.Migration.Analyzers.csproj b/Source/Mockolate.Migration.Analyzers/Mockolate.Migration.Analyzers.csproj new file mode 100644 index 0000000..ec5d379 --- /dev/null +++ b/Source/Mockolate.Migration.Analyzers/Mockolate.Migration.Analyzers.csproj @@ -0,0 +1,42 @@ + + + + Analyzers for Mockolate to migrate from other mocking libraries. + true + true + false + true + RS2003 + false + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + True + True + Resources.resx + + + + diff --git a/Source/Mockolate.Migration.Analyzers/MoqAnalyzer.cs b/Source/Mockolate.Migration.Analyzers/MoqAnalyzer.cs new file mode 100644 index 0000000..233bbb7 --- /dev/null +++ b/Source/Mockolate.Migration.Analyzers/MoqAnalyzer.cs @@ -0,0 +1,73 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Mockolate.Migration.Analyzers.Common; + +namespace Mockolate.Migration.Analyzers; + +/// +/// An analyzer that flags mock usage from Moq. +/// +/// +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class MoqAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [Rules.MoqRule,]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterOperationAction(AnalyzeOperation, OperationKind.Invocation); + context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation); + } + + private static void AnalyzeOperation(OperationAnalysisContext context) + { + if (context.Operation is IInvocationOperation invocationOperation) + { + IMethodSymbol? methodSymbol = invocationOperation.TargetMethod; + + string? fullyQualifiedNonGenericMethodName = methodSymbol.GloballyQualifiedNonGeneric(); + + if (fullyQualifiedNonGenericMethodName.StartsWith("global::Moq") && + fullyQualifiedNonGenericMethodName.EndsWith("Mock")) + { + SyntaxNode syntax = invocationOperation.Syntax; + while (syntax.Parent is ExpressionOrPatternSyntax && syntax.Parent is not AwaitExpressionSyntax) + { + syntax = syntax.Parent; + } + + // Do not report nested `.Should()` e.g. in `.Should().AllSatisfy(x => x.Should().BeGreaterThan(0));` + if (syntax.Parent is not ArgumentSyntax) + { + context.ReportDiagnostic( + Diagnostic.Create(Rules.MoqRule, syntax.GetLocation()) + ); + } + } + } + } + + private static void AnalyzeObjectCreation(OperationAnalysisContext context) + { + if (context.Operation is IObjectCreationOperation objectCreationOperation) + { + INamedTypeSymbol? typeSymbol = objectCreationOperation.Constructor?.ContainingType; + if (typeSymbol != null && typeSymbol.ContainingNamespace.ToDisplayString() == "Moq" && typeSymbol.Name == "Mock") + { + context.ReportDiagnostic( + Diagnostic.Create(Rules.MoqRule, objectCreationOperation.Syntax.GetLocation()) + ); + } + } + } +} diff --git a/Source/Mockolate.Migration.Analyzers/Resources.Designer.cs b/Source/Mockolate.Migration.Analyzers/Resources.Designer.cs new file mode 100644 index 0000000..158affe --- /dev/null +++ b/Source/Mockolate.Migration.Analyzers/Resources.Designer.cs @@ -0,0 +1,98 @@ +//------------------------------------------------------------------------------ +// +// 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 Mockolate.Migration.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("Mockolate.Migration.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 Migrate Moq to Mockolate. + /// + internal static string MockolateM001CodeFixTitle { + get { + return ResourceManager.GetString("MockolateM001CodeFixTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Migrate the mocks from Moq to Mockolate.. + /// + internal static string MockolateM001Description { + get { + return ResourceManager.GetString("MockolateM001Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Moq should be migrated to Mockolate.. + /// + internal static string MockolateM001MessageFormat { + get { + return ResourceManager.GetString("MockolateM001MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Moq should be migrated.. + /// + internal static string MockolateM001Title { + get { + return ResourceManager.GetString("MockolateM001Title", resourceCulture); + } + } + } +} diff --git a/Source/Mockolate.Migration.Analyzers/Resources.resx b/Source/Mockolate.Migration.Analyzers/Resources.resx new file mode 100644 index 0000000..be46d67 --- /dev/null +++ b/Source/Mockolate.Migration.Analyzers/Resources.resx @@ -0,0 +1,40 @@ + + + + + + + + + + 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 + + + + Migrate the mocks from Moq to Mockolate. + + + Moq should be migrated to Mockolate. + + + Moq should be migrated. + + + Migrate Moq to Mockolate + The title of the code fix. + + diff --git a/Source/Mockolate.Migration.Analyzers/Rules.cs b/Source/Mockolate.Migration.Analyzers/Rules.cs new file mode 100644 index 0000000..3e4a391 --- /dev/null +++ b/Source/Mockolate.Migration.Analyzers/Rules.cs @@ -0,0 +1,32 @@ +using Microsoft.CodeAnalysis; + +namespace Mockolate.Migration.Analyzers; + +/// +/// The rules for the analyzers in this project. +/// +public static class Rules +{ + private const string UsageCategory = "Usage"; + + /// + /// Migration rule for Moq usage. Flags any usage of `new Mock<T>()` or `new Mock<T>()` with target-typed new. + /// + public static readonly DiagnosticDescriptor MoqRule = + CreateDescriptor("MockolateM001", UsageCategory, DiagnosticSeverity.Warning); + + + private static DiagnosticDescriptor CreateDescriptor(string diagnosticId, string category, + DiagnosticSeverity severity) => new( + diagnosticId, + new LocalizableResourceString(diagnosticId + "Title", + Resources.ResourceManager, typeof(Resources)), + new LocalizableResourceString(diagnosticId + "MessageFormat", Resources.ResourceManager, + typeof(Resources)), + category, + severity, + true, + new LocalizableResourceString(diagnosticId + "Description", Resources.ResourceManager, + typeof(Resources)) + ); +} diff --git a/Tests/Directory.Build.props b/Tests/Directory.Build.props index d053396..b9d8c75 100644 --- a/Tests/Directory.Build.props +++ b/Tests/Directory.Build.props @@ -4,7 +4,7 @@ Condition="Exists('$(MSBuildThisFileDirectory)/../Directory.Build.props')"/> - net10.0;net8.0;net48 + net10.0 @@ -25,10 +25,6 @@ all - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Tests/Mockolate.Migration.Api.Tests/ApiAcceptance.cs b/Tests/Mockolate.Migration.Api.Tests/ApiAcceptance.cs deleted file mode 100644 index 6fac390..0000000 --- a/Tests/Mockolate.Migration.Api.Tests/ApiAcceptance.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace aweXpect.Api.Tests; - -public sealed class ApiAcceptance -{ - /// - /// Execute this test to update the expected public API to the current API surface. - /// - [Fact(Explicit = true)] - public async Task AcceptApiChanges() - { - string[] assemblyNames = - [ - "Mockolate.Migration", - ]; - - foreach (string assemblyName in assemblyNames) - { - foreach (string framework in Helper.GetTargetFrameworks()) - { - string publicApi = Helper.CreatePublicApi(framework, assemblyName) - .Replace("\n", Environment.NewLine); - Helper.SetExpectedApi(framework, assemblyName, publicApi); - } - } - - await That(assemblyNames).IsNotEmpty(); - } -} diff --git a/Tests/Mockolate.Migration.Api.Tests/ApiApprovalTests.cs b/Tests/Mockolate.Migration.Api.Tests/ApiApprovalTests.cs deleted file mode 100644 index 7c709fb..0000000 --- a/Tests/Mockolate.Migration.Api.Tests/ApiApprovalTests.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace aweXpect.Api.Tests; - -/// -/// Whenever a test fails, this means that the public API surface changed. -/// If the change was intentional, execute the test to take over the -/// current public API surface. The changes will become part of the pull request and will be reviewed accordingly. -/// -public sealed class ApiApprovalTests -{ - [Theory] - [MemberData(nameof(TargetFrameworksTheoryData))] - public async Task VerifyPublicApiForAweXpectT6e(string framework) - { - const string assemblyName = "Mockolate.Migration"; - - string publicApi = Helper.CreatePublicApi(framework, assemblyName); - string expectedApi = Helper.GetExpectedApi(framework, assemblyName); - - await That(publicApi).IsEqualTo(expectedApi); - } - - public static TheoryData TargetFrameworksTheoryData() - { - TheoryData theoryData = new(); - foreach (string targetFramework in Helper.GetTargetFrameworks()) - { - theoryData.Add(targetFramework); - } - - return theoryData; - } -} diff --git a/Tests/Mockolate.Migration.Api.Tests/Expected/Mockolate.Migration_net10.0.txt b/Tests/Mockolate.Migration.Api.Tests/Expected/Mockolate.Migration_net10.0.txt deleted file mode 100644 index 913a416..0000000 --- a/Tests/Mockolate.Migration.Api.Tests/Expected/Mockolate.Migration_net10.0.txt +++ /dev/null @@ -1,2 +0,0 @@ -[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/aweXpect/Mockolate.Migration.git")] -[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v10.0", FrameworkDisplayName=".NET 10.0")] diff --git a/Tests/Mockolate.Migration.Api.Tests/Expected/Mockolate.Migration_net8.0.txt b/Tests/Mockolate.Migration.Api.Tests/Expected/Mockolate.Migration_net8.0.txt deleted file mode 100644 index 7c1510f..0000000 --- a/Tests/Mockolate.Migration.Api.Tests/Expected/Mockolate.Migration_net8.0.txt +++ /dev/null @@ -1,2 +0,0 @@ -[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/aweXpect/Mockolate.Migration.git")] -[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v8.0", FrameworkDisplayName=".NET 8.0")] diff --git a/Tests/Mockolate.Migration.Api.Tests/Expected/Mockolate.Migration_netstandard2.0.txt b/Tests/Mockolate.Migration.Api.Tests/Expected/Mockolate.Migration_netstandard2.0.txt deleted file mode 100644 index 48c4bdc..0000000 --- a/Tests/Mockolate.Migration.Api.Tests/Expected/Mockolate.Migration_netstandard2.0.txt +++ /dev/null @@ -1,2 +0,0 @@ -[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/aweXpect/Mockolate.Migration.git")] -[assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName=".NET Standard 2.0")] diff --git a/Tests/Mockolate.Migration.Api.Tests/Helper.cs b/Tests/Mockolate.Migration.Api.Tests/Helper.cs deleted file mode 100644 index 6076e2b..0000000 --- a/Tests/Mockolate.Migration.Api.Tests/Helper.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Xml.Linq; -using System.Xml.XPath; -using PublicApiGenerator; - -namespace aweXpect.Api.Tests; - -public static class Helper -{ - public static string CreatePublicApi(string framework, string assemblyName) - { -#if DEBUG - string configuration = "Debug"; -#else - string configuration = "Release"; -#endif - string assemblyFile = - CombinedPaths("Source", assemblyName, "bin", configuration, framework, $"{assemblyName}.dll"); - Assembly assembly = Assembly.LoadFile(assemblyFile); - string publicApi = assembly.GeneratePublicApi(); - return publicApi.Replace("\r\n", "\n"); - } - - public static string GetExpectedApi(string framework, string assemblyName) - { - string expectedPath = CombinedPaths("Tests", "Mockolate.Migration.Api.Tests", - "Expected", $"{assemblyName}_{framework}.txt"); - try - { - return File.ReadAllText(expectedPath) - .Replace("\r\n", "\n"); - } - catch - { - return string.Empty; - } - } - - public static IEnumerable GetTargetFrameworks() - { - string csproj = CombinedPaths("Source", "Directory.Build.props"); - XDocument project = XDocument.Load(csproj); - XElement? targetFrameworks = - project.XPathSelectElement("/Project/PropertyGroup/TargetFrameworks"); - foreach (string targetFramework in targetFrameworks!.Value.Split(';')) - { - yield return targetFramework; - } - } - - public static void SetExpectedApi(string framework, string assemblyName, string publicApi) - { - string expectedPath = CombinedPaths("Tests", "Mockolate.Migration.Api.Tests", - "Expected", $"{assemblyName}_{framework}.txt"); - Directory.CreateDirectory(Path.GetDirectoryName(expectedPath)!); - File.WriteAllText(expectedPath, publicApi); - } - - private static string CombinedPaths(params string[] paths) => - Path.GetFullPath(Path.Combine(paths.Prepend(GetSolutionDirectory()).ToArray())); - - private static string GetSolutionDirectory([CallerFilePath] string path = "") => - Path.Combine(Path.GetDirectoryName(path)!, "..", ".."); -} diff --git a/Tests/Mockolate.Migration.Api.Tests/Mockolate.Migration.Api.Tests.csproj b/Tests/Mockolate.Migration.Api.Tests/Mockolate.Migration.Api.Tests.csproj deleted file mode 100644 index 46fd666..0000000 --- a/Tests/Mockolate.Migration.Api.Tests/Mockolate.Migration.Api.Tests.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - net10.0 - - - - - - - - - - - - - - diff --git a/Tests/Mockolate.Migration.Example.Tests/Examples.cs b/Tests/Mockolate.Migration.Example.Tests/Examples.cs new file mode 100644 index 0000000..dd2a523 --- /dev/null +++ b/Tests/Mockolate.Migration.Example.Tests/Examples.cs @@ -0,0 +1,21 @@ +using Moq; + +namespace Mockolate.Migration.Example.Tests; + +public class Examples +{ + [Fact] + public async Task MoqCreation() + { + Mock sut = new(); + + sut.Setup(m => m.Dispense(It.IsAny(), It.Is(x => x > 0))) + .Returns(true); + + IChocolateDispenser x = sut.Object; + + bool result = x.Dispense("Dark", 1); + + await That(result).IsTrue(); + } +} diff --git a/Tests/Mockolate.Migration.Example.Tests/IChocolateDispenser.cs b/Tests/Mockolate.Migration.Example.Tests/IChocolateDispenser.cs new file mode 100644 index 0000000..2d3da2f --- /dev/null +++ b/Tests/Mockolate.Migration.Example.Tests/IChocolateDispenser.cs @@ -0,0 +1,11 @@ +namespace Mockolate.Migration.Example.Tests; + +public delegate void ChocolateDispensedDelegate(string type, int amount); + +public interface IChocolateDispenser +{ + int this[string type] { get; set; } + int TotalDispensed { get; set; } + bool Dispense(string type, int amount); + event ChocolateDispensedDelegate ChocolateDispensed; +} diff --git a/Tests/Mockolate.Migration.Example.Tests/Mockolate.Migration.Example.Tests.csproj b/Tests/Mockolate.Migration.Example.Tests/Mockolate.Migration.Example.Tests.csproj new file mode 100644 index 0000000..a197d9e --- /dev/null +++ b/Tests/Mockolate.Migration.Example.Tests/Mockolate.Migration.Example.Tests.csproj @@ -0,0 +1,27 @@ + + + + Mockolate.Migration.Example.Tests + + + + + + + + + + + + + + + diff --git a/Tests/Mockolate.Migration.Api.Tests/Usings.cs b/Tests/Mockolate.Migration.Example.Tests/Usings.cs similarity index 100% rename from Tests/Mockolate.Migration.Api.Tests/Usings.cs rename to Tests/Mockolate.Migration.Example.Tests/Usings.cs diff --git a/Tests/Mockolate.Migration.Tests/DummyTests.cs b/Tests/Mockolate.Migration.Tests/DummyTests.cs deleted file mode 100644 index ceb18ae..0000000 --- a/Tests/Mockolate.Migration.Tests/DummyTests.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Mockolate.Migration.Tests; - -public sealed class DummyTests -{ - [Fact] - public async Task WhenPathIsAbsolute_ShouldSucceed() - { - string path = "/foo"; - - async Task Act() - => await That(path).IsNotEmpty(); - - await That(Act).DoesNotThrow(); - } -} diff --git a/Tests/Mockolate.Migration.Tests/Mockolate.Migration.Tests.csproj b/Tests/Mockolate.Migration.Tests/Mockolate.Migration.Tests.csproj index 40b4e23..9db0cc1 100644 --- a/Tests/Mockolate.Migration.Tests/Mockolate.Migration.Tests.csproj +++ b/Tests/Mockolate.Migration.Tests/Mockolate.Migration.Tests.csproj @@ -1,7 +1,17 @@ + + + + + + + + + + diff --git a/Tests/Mockolate.Migration.Tests/MoqAnalyzerTests.cs b/Tests/Mockolate.Migration.Tests/MoqAnalyzerTests.cs new file mode 100644 index 0000000..d3f9cd1 --- /dev/null +++ b/Tests/Mockolate.Migration.Tests/MoqAnalyzerTests.cs @@ -0,0 +1,43 @@ +using Mockolate.Migration.Analyzers; +using Verifier = Mockolate.Migration.Tests.Verifiers.CSharpAnalyzerVerifier; + +namespace Mockolate.Migration.Tests; + +public class MoqAnalyzerTests +{ + [Fact] + public async Task NewMockExplicit_IsFlagged() + => await Verifier.VerifyAnalyzerAsync(""" + using Moq; + + public interface IFoo { } + + public class Tests + { + public void Test() + { + var mock = {|#0:new Mock()|}; + } + } + """, + Verifier.Diagnostic(Rules.MoqRule) + .WithLocation(0)); + + [Fact] + public async Task NewMockTargetTyped_IsFlagged() + => await Verifier.VerifyAnalyzerAsync(""" + using Moq; + + public interface IFoo { } + + public class Tests + { + public void Test() + { + Mock mock = {|#0:new()|}; + } + } + """, + Verifier.Diagnostic(Rules.MoqRule) + .WithLocation(0)); +} diff --git a/Tests/Mockolate.Migration.Tests/Verifiers/CSharpAnalyzerVerifier.cs b/Tests/Mockolate.Migration.Tests/Verifiers/CSharpAnalyzerVerifier.cs new file mode 100644 index 0000000..ff0957c --- /dev/null +++ b/Tests/Mockolate.Migration.Tests/Verifiers/CSharpAnalyzerVerifier.cs @@ -0,0 +1,49 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; + +namespace Mockolate.Migration.Tests.Verifiers; + +public static partial class CSharpAnalyzerVerifier + where TAnalyzer : DiagnosticAnalyzer, new() +{ + public class Test : CSharpAnalyzerTest + { + public Test() + { + SolutionTransforms.Add((solution, projectId) => + { + Project? project = solution.GetProject(projectId); + + if (project is null) + { + return solution; + } + + CompilationOptions? compilationOptions = project.CompilationOptions; + + if (compilationOptions is null) + { + return solution; + } + + CSharpParseOptions? 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; + }); + } + } +} diff --git a/Tests/Mockolate.Migration.Tests/Verifiers/CSharpAnalyzerVerifier`1.cs b/Tests/Mockolate.Migration.Tests/Verifiers/CSharpAnalyzerVerifier`1.cs new file mode 100644 index 0000000..b3c9996 --- /dev/null +++ b/Tests/Mockolate.Migration.Tests/Verifiers/CSharpAnalyzerVerifier`1.cs @@ -0,0 +1,49 @@ +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; + +namespace Mockolate.Migration.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([StringSyntax("c#-test")] string source, + params DiagnosticResult[] expected) + { + Test test = new() + { + TestCode = source, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80.AddPackages( + [ + new PackageIdentity("Moq", "4.20.72"), + ]), + TestState = + { + AdditionalReferences = + { + typeof(Expect).Assembly.Location, + typeof(ThatBool).Assembly.Location, + }, + }, + }; + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } +} diff --git a/Tests/Mockolate.Migration.Tests/Verifiers/CSharpCodeFixVerifier.cs b/Tests/Mockolate.Migration.Tests/Verifiers/CSharpCodeFixVerifier.cs new file mode 100644 index 0000000..8ea8303 --- /dev/null +++ b/Tests/Mockolate.Migration.Tests/Verifiers/CSharpCodeFixVerifier.cs @@ -0,0 +1,51 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; + +namespace Mockolate.Migration.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) => + { + Project? project = solution.GetProject(projectId); + + if (project is null) + { + return solution; + } + + CompilationOptions? compilationOptions = project.CompilationOptions; + + if (compilationOptions is null) + { + return solution; + } + + CSharpParseOptions? 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; + }); + } + } +} diff --git a/Tests/Mockolate.Migration.Tests/Verifiers/CSharpCodeFixVerifier`2.cs b/Tests/Mockolate.Migration.Tests/Verifiers/CSharpCodeFixVerifier`2.cs new file mode 100644 index 0000000..26207b5 --- /dev/null +++ b/Tests/Mockolate.Migration.Tests/Verifiers/CSharpCodeFixVerifier`2.cs @@ -0,0 +1,121 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; + +namespace Mockolate.Migration.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( + [StringSyntax("c#-test")] string source, + params DiagnosticResult[] expected + ) + { + Test test = new() + { + TestCode = source, + CodeActionValidationMode = CodeActionValidationMode.SemanticStructure, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80.AddPackages( + [ + new PackageIdentity("Moq", "4.20.72"), + ]), + }; + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } + + /// + public static async Task VerifyCodeFixAsync([StringSyntax("c#-test")] string source, + [StringSyntax("c#-test")] string fixedSource) + => await VerifyCodeFixAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource); + + /// + public static async Task VerifyCodeFixAsync([StringSyntax("c#-test")] string source, DiagnosticResult expected, + [StringSyntax("c#-test")] string fixedSource) + => await VerifyCodeFixAsync(source, [expected,], fixedSource); + + /// + public static async Task VerifyCodeFixAsync( + [StringSyntax("c#-test")] string source, + IEnumerable expected, + [StringSyntax("c#-test")] string fixedSource + ) + { + Test test = new() + { + TestCode = source, + FixedCode = fixedSource, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80.AddPackages( + [ + new PackageIdentity("Moq", "4.20.72"), + ]), + TestState = + { + AdditionalReferences = + { + typeof(Expect).Assembly.Location, + typeof(ThatBool).Assembly.Location, + }, + }, + }; + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } + + public static async Task VerifyLegacyCodeFixAsync([StringSyntax("c#-test")] string source, + [StringSyntax("c#-test")] string fixedSource) + => await VerifyLegacyCodeFixAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource); + + /// + public static async Task VerifyLegacyCodeFixAsync( + [StringSyntax("c#-test")] string source, + IEnumerable expected, + [StringSyntax("c#-test")] string fixedSource + ) + { + Test test = new() + { + TestCode = source, + FixedCode = fixedSource, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80.AddPackages( + [ + new PackageIdentity("Moq", "4.20.72"), + ]), + TestState = + { + AdditionalReferences = + { + typeof(Expect).Assembly.Location, + typeof(ThatBool).Assembly.Location, + }, + }, + }; + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } +} diff --git a/Tests/Mockolate.Migration.Tests/Verifiers/CSharpCodeRefactoringVerifier.cs b/Tests/Mockolate.Migration.Tests/Verifiers/CSharpCodeRefactoringVerifier.cs new file mode 100644 index 0000000..1841ba2 --- /dev/null +++ b/Tests/Mockolate.Migration.Tests/Verifiers/CSharpCodeRefactoringVerifier.cs @@ -0,0 +1,49 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; + +namespace Mockolate.Migration.Tests.Verifiers; + +public static partial class CSharpCodeRefactoringVerifier + where TCodeRefactoring : CodeRefactoringProvider, new() +{ + public class Test : CSharpCodeRefactoringTest + { + public Test() + { + SolutionTransforms.Add((solution, projectId) => + { + Project? project = solution.GetProject(projectId); + + if (project is null) + { + return solution; + } + + CompilationOptions? compilationOptions = project.CompilationOptions; + + if (compilationOptions is null) + { + return solution; + } + + CSharpParseOptions? 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; + }); + } + } +} diff --git a/Tests/Mockolate.Migration.Tests/Verifiers/CSharpCodeRefactoringVerifier`1.cs b/Tests/Mockolate.Migration.Tests/Verifiers/CSharpCodeRefactoringVerifier`1.cs new file mode 100644 index 0000000..a57472d --- /dev/null +++ b/Tests/Mockolate.Migration.Tests/Verifiers/CSharpCodeRefactoringVerifier`1.cs @@ -0,0 +1,32 @@ +using System.Threading; +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.Testing; + +namespace Mockolate.Migration.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, [expected,], fixedSource); + + /// + public static async Task VerifyRefactoringAsync(string source, DiagnosticResult[] expected, string fixedSource) + { + Test test = new() + { + TestCode = source, + FixedCode = fixedSource, + }; + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } +} diff --git a/Tests/Mockolate.Migration.Tests/Verifiers/CSharpVerifierHelper.cs b/Tests/Mockolate.Migration.Tests/Verifiers/CSharpVerifierHelper.cs new file mode 100644 index 0000000..b466543 --- /dev/null +++ b/Tests/Mockolate.Migration.Tests/Verifiers/CSharpVerifierHelper.cs @@ -0,0 +1,35 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Mockolate.Migration.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",]; + CSharpCommandLineArguments commandLineArguments = + CSharpCommandLineParser.Default.Parse(args, Environment.CurrentDirectory, Environment.CurrentDirectory); + ImmutableDictionary 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; + } +} From 94616a3f00014287a2453a06000f4cfa2d48512e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sat, 4 Apr 2026 21:02:32 +0200 Subject: [PATCH 2/3] feat: add initial analyzer that detects Moq --- Tests/Mockolate.Migration.Example.Tests/Examples.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/Mockolate.Migration.Example.Tests/Examples.cs b/Tests/Mockolate.Migration.Example.Tests/Examples.cs index dd2a523..73477d4 100644 --- a/Tests/Mockolate.Migration.Example.Tests/Examples.cs +++ b/Tests/Mockolate.Migration.Example.Tests/Examples.cs @@ -7,7 +7,9 @@ public class Examples [Fact] public async Task MoqCreation() { +#pragma warning disable MockolateM001 Mock sut = new(); +#pragma warning restore MockolateM001 sut.Setup(m => m.Dispense(It.IsAny(), It.Is(x => x > 0))) .Returns(true); From 682e8e42f16f7142343eabe3622755faa195fe55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sat, 4 Apr 2026 21:07:25 +0200 Subject: [PATCH 3/3] Remove benchmarks --- .github/workflows/build.yml | 23 +-- .github/workflows/ci.yml | 25 --- .nuke/build.schema.json | 9 -- .../HappyCaseBenchmarks.Dummy.cs | 13 -- .../HappyCaseBenchmarks.cs | 22 --- .../Mockolate.Migration.Benchmarks.csproj | 20 --- .../Mockolate.Migration.Benchmarks/Program.cs | 3 - Mockolate.Migration.slnx | 3 - Pipeline/Build.Benchmarks.cs | 149 ------------------ Pipeline/Build.cs | 2 +- .../MoqCodeFixProvider.cs | 2 +- 11 files changed, 3 insertions(+), 268 deletions(-) delete mode 100644 Benchmarks/Mockolate.Migration.Benchmarks/HappyCaseBenchmarks.Dummy.cs delete mode 100644 Benchmarks/Mockolate.Migration.Benchmarks/HappyCaseBenchmarks.cs delete mode 100644 Benchmarks/Mockolate.Migration.Benchmarks/Mockolate.Migration.Benchmarks.csproj delete mode 100644 Benchmarks/Mockolate.Migration.Benchmarks/Program.cs delete mode 100644 Pipeline/Build.Benchmarks.cs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9647fe7..11854ff 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,26 +37,6 @@ jobs: ./Artifacts/* ./TestResults/*.trx - benchmarks: - name: "Benchmarks" - runs-on: ubuntu-latest - permissions: - contents: write - env: - DOTNET_NOLOGO: true - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - name: Setup .NET SDKs - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 8.0.x - 9.0.x - - name: Run benchmarks - run: ./build.sh Benchmarks - static-code-analysis: name: "Static code analysis" runs-on: ubuntu-latest @@ -99,7 +79,7 @@ jobs: pack: name: "Pack" runs-on: ubuntu-latest - needs: [ publish-test-results, benchmarks, static-code-analysis ] + needs: [ publish-test-results, static-code-analysis ] env: DOTNET_NOLOGO: true steps: @@ -197,7 +177,6 @@ jobs: build-pages: name: Update Pages runs-on: ubuntu-latest - needs: [ benchmarks ] steps: - name: Trigger pages update in aweXpect Repo run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a525ee..324ac88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,31 +37,6 @@ jobs: ./Artifacts/* ./TestResults/*.trx - benchmarks: - name: "Benchmarks" - runs-on: ubuntu-latest - env: - DOTNET_NOLOGO: true - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - name: Setup .NET SDKs - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 8.0.x - 9.0.x - - name: Run benchmarks - run: ./build.sh Benchmarks - - name: Upload artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: Benchmarks - path: | - ./Artifacts/* - static-code-analysis: name: "Static code analysis" if: ${{ github.actor != 'dependabot[bot]'&& github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index e631187..26b7e09 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -24,11 +24,6 @@ "ExecutableTarget": { "type": "string", "enum": [ - "ApiChecks", - "BenchmarkComment", - "BenchmarkDotNet", - "BenchmarkResult", - "Benchmarks", "CalculateNugetVersion", "Clean", "CodeAnalysis", @@ -37,10 +32,6 @@ "CodeCoverage", "Compile", "DotNetUnitTests", - "MutationComment", - "MutationTestDashboard", - "MutationTestExecution", - "MutationTests", "Pack", "Restore", "UnitTests", diff --git a/Benchmarks/Mockolate.Migration.Benchmarks/HappyCaseBenchmarks.Dummy.cs b/Benchmarks/Mockolate.Migration.Benchmarks/HappyCaseBenchmarks.Dummy.cs deleted file mode 100644 index 1f49d89..0000000 --- a/Benchmarks/Mockolate.Migration.Benchmarks/HappyCaseBenchmarks.Dummy.cs +++ /dev/null @@ -1,13 +0,0 @@ -using BenchmarkDotNet.Attributes; - -namespace Mockolate.Migration.Benchmarks; - -/// -/// This is a dummy benchmark in the T6e template. -/// -public partial class HappyCaseBenchmarks -{ - [Benchmark] - public TimeSpan Dummy_aweXpect() - => TimeSpan.FromSeconds(10); -} diff --git a/Benchmarks/Mockolate.Migration.Benchmarks/HappyCaseBenchmarks.cs b/Benchmarks/Mockolate.Migration.Benchmarks/HappyCaseBenchmarks.cs deleted file mode 100644 index dcddbdb..0000000 --- a/Benchmarks/Mockolate.Migration.Benchmarks/HappyCaseBenchmarks.cs +++ /dev/null @@ -1,22 +0,0 @@ -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Toolchains.InProcess.Emit; - -namespace Mockolate.Migration.Benchmarks; - -[MarkdownExporterAttribute.GitHub] -[MemoryDiagnoser] -public partial class HappyCaseBenchmarks -{ - private class Config : ManualConfig - { - public Config() - { - AddJob(Job.MediumRun - .WithLaunchCount(1) - .WithToolchain(InProcessEmitToolchain.Instance) - .WithId("InProcess")); - } - } -} diff --git a/Benchmarks/Mockolate.Migration.Benchmarks/Mockolate.Migration.Benchmarks.csproj b/Benchmarks/Mockolate.Migration.Benchmarks/Mockolate.Migration.Benchmarks.csproj deleted file mode 100644 index bb040cf..0000000 --- a/Benchmarks/Mockolate.Migration.Benchmarks/Mockolate.Migration.Benchmarks.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - Exe - net10.0 - enable - enable - false - false - - - - - - - - - - - diff --git a/Benchmarks/Mockolate.Migration.Benchmarks/Program.cs b/Benchmarks/Mockolate.Migration.Benchmarks/Program.cs deleted file mode 100644 index b986d99..0000000 --- a/Benchmarks/Mockolate.Migration.Benchmarks/Program.cs +++ /dev/null @@ -1,3 +0,0 @@ -using BenchmarkDotNet.Running; - -BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); diff --git a/Mockolate.Migration.slnx b/Mockolate.Migration.slnx index 3118d4d..1e6a64f 100644 --- a/Mockolate.Migration.slnx +++ b/Mockolate.Migration.slnx @@ -1,7 +1,4 @@ - - - diff --git a/Pipeline/Build.Benchmarks.cs b/Pipeline/Build.Benchmarks.cs deleted file mode 100644 index 7996458..0000000 --- a/Pipeline/Build.Benchmarks.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using Nuke.Common; -using Nuke.Common.IO; -using Nuke.Common.Tools.DotNet; -using Octokit; -using Serilog; -using static Nuke.Common.Tools.DotNet.DotNetTasks; - -// ReSharper disable AllUnderscoreLocalParameterName - -namespace Build; - -partial class Build -{ - Target BenchmarkDotNet => _ => _ - .Executes(() => - { - AbsolutePath benchmarkDirectory = ArtifactsDirectory / "Benchmarks"; - benchmarkDirectory.CreateOrCleanDirectory(); - - DotNetBuild(s => s - .SetProjectFile(Solution.Benchmarks.Mockolate_Migration_Benchmarks) - .SetConfiguration(Configuration.Release) - .EnableNoLogo()); - - DotNet( - $"{Solution.Benchmarks.Mockolate_Migration_Benchmarks.Name}.dll --exporters json --filter * --artifacts \"{benchmarkDirectory}\"", - Solution.Benchmarks.Mockolate_Migration_Benchmarks.Directory / "bin" / "Release"); - }); - - Target BenchmarkResult => _ => _ - .After(BenchmarkDotNet) - .Executes(async () => - { - string fileContent = await File.ReadAllTextAsync(ArtifactsDirectory / "Benchmarks" / "results" / - "Mockolate.Migration.Benchmarks.HappyCaseBenchmarks-report-github.md"); - Log.Information("Report:\n {FileContent}", fileContent); - if (GitHubActions?.IsPullRequest == true) - { - File.WriteAllText(ArtifactsDirectory / "PR.txt", GitHubActions.PullRequestNumber.ToString()); - } - }); - - Target BenchmarkComment => _ => _ - .Executes(async () => - { - await "Benchmarks".DownloadArtifactTo(ArtifactsDirectory, GithubToken); - if (!File.Exists(ArtifactsDirectory / "PR.txt")) - { - Log.Information("Skip writing a comment, as no PR number was specified."); - return; - } - - string prNumber = File.ReadAllText(ArtifactsDirectory / "PR.txt"); - string body = CreateBenchmarkCommentBody(); - Log.Debug("Pull request number: {PullRequestId}", prNumber); - if (int.TryParse(prNumber, out int prId)) - { - GitHubClient gitHubClient = new(new ProductHeaderValue("Nuke")); - Credentials tokenAuth = new(GithubToken); - gitHubClient.Credentials = tokenAuth; - IReadOnlyList comments = - await gitHubClient.Issue.Comment.GetAllForIssue("aweXpect", "Mockolate.Migration", prId); - long? commentId = null; - Log.Information($"Found {comments.Count} comments"); - foreach (IssueComment comment in comments) - { - if (comment.Body.Contains("## :rocket: Benchmark Results")) - { - Log.Information($"Found comment: {comment.Body}"); - commentId = comment.Id; - } - } - - if (commentId == null) - { - Log.Information($"Create comment:\n{body}"); - await gitHubClient.Issue.Comment.Create("aweXpect", "Mockolate.Migration", prId, body); - } - else - { - Log.Information($"Update comment:\n{body}"); - await gitHubClient.Issue.Comment.Update("aweXpect", "Mockolate.Migration", commentId.Value, body); - } - } - }); - - Target Benchmarks => _ => _ - .DependsOn(BenchmarkDotNet) - .DependsOn(BenchmarkResult); - - string CreateBenchmarkCommentBody() - { - string[] fileContent = File.ReadAllLines(ArtifactsDirectory / "Benchmarks" / "results" / - "Mockolate.Migration.Benchmarks.HappyCaseBenchmarks-report-github.md"); - StringBuilder sb = new(); - sb.AppendLine("## :rocket: Benchmark Results"); - sb.AppendLine("
"); - sb.AppendLine("Details"); - int count = 0; - foreach (string line in fileContent) - { - if (line.StartsWith("```")) - { - count++; - if (count == 1) - { - sb.AppendLine("
");
-				}
-				else if (count == 2)
-				{
-					sb.AppendLine("
"); - sb.AppendLine("
"); - sb.AppendLine(); - } - - continue; - } - - if (line.StartsWith('|') && line.Contains("_aweXpect") && line.EndsWith('|')) - { - MakeLineBold(sb, line); - continue; - } - - sb.AppendLine(line); - } - - string body = sb.ToString(); - return body; - } - - static void MakeLineBold(StringBuilder sb, string line) - { - string[] tokens = line.Split("|", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); - sb.Append('|'); - foreach (string token in tokens) - { - sb.Append(" **"); - sb.Append(token); - sb.Append("** |"); - } - - sb.AppendLine(); - } -} diff --git a/Pipeline/Build.cs b/Pipeline/Build.cs index 7aa34f7..e9ef8cd 100644 --- a/Pipeline/Build.cs +++ b/Pipeline/Build.cs @@ -27,5 +27,5 @@ partial class Build : NukeBuild AbsolutePath TestResultsDirectory => RootDirectory / "TestResults"; GitHubActions GitHubActions => GitHubActions.Instance; - public static int Main() => Execute(x => x.Benchmarks, x => x.CodeAnalysis); + public static int Main() => Execute(x => x.CodeAnalysis); } diff --git a/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs b/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs index b301136..04ddfba 100644 --- a/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs +++ b/Source/Mockolate.Migration.Analyzers.CodeFixers/MoqCodeFixProvider.cs @@ -23,7 +23,7 @@ protected override async Task ConvertAssertionAsync(CodeFixContext con SyntaxNode? root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (root is not CompilationUnitSyntax compilationUnit) + if (root is not CompilationUnitSyntax) { return document; }