diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index cbecc491..f1ae4295 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "nbgv": { - "version": "3.6.133", + "version": "3.6.139", "commands": [ "nbgv" ], diff --git a/.editorconfig b/.editorconfig index e7d7b135..f2384a03 100644 --- a/.editorconfig +++ b/.editorconfig @@ -82,6 +82,8 @@ dotnet_style_readonly_field = true:warning # Parameter preferences dotnet_code_quality_unused_parameters = all:suggestion +# AV1561: Signature contains too many parameters +dotnet_diagnostic.AV1561.severity = suggestion # Suppression preferences dotnet_remove_unnecessary_suppression_exclusions = none @@ -89,6 +91,8 @@ dotnet_remove_unnecessary_suppression_exclusions = none # XMLDocs preferences # SA1600: Elements should be documented. We disable this it requires xmldocs for _all_ members. CS1591 already covers documenting public members. dotnet_diagnostic.SA1600.severity = silent +# AV2305: Missing XML comment for internally visible type, member or parameter +dotnet_diagnostic.AV2305.severity = silent #### C# Coding Conventions #### [*.cs] @@ -238,6 +242,9 @@ dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase +# SA1309: Field names must not begin with underscore +# Keep this aligned with `dotnet_naming_rule.private_fields_should_be__camelcase.style` +dotnet_diagnostic.SA1309.severity = none dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields @@ -381,7 +388,31 @@ dotnet_naming_style.s_camelcase.required_suffix = dotnet_naming_style.s_camelcase.word_separator = dotnet_naming_style.s_camelcase.capitalization = camel_case +# AV1580: Method argument calls a nested method +# Because debugger breakpoints cannot be set inside expressions, avoid overuse of nested method calls. +# Example: string result = ConvertToXml(ApplyTransforms(ExecuteQuery(GetConfigurationSettings(source)))); +# requires extra steps to inspect intermediate method return values. On the other hard, were this expression broken into intermediate variables, setting a breakpoint on one of them would be sufficient. +# +# This is moved to silent because it's flagging foo.AsSpan() +dotnet_diagnostic.AV1580.severity = silent + # MA0040: Forward the CancellationToken parameter to methods that take one dotnet_diagnostic.MA0040.severity = error # Async analyzer -dotnet_diagnostic.CA2016.severity = error \ No newline at end of file +dotnet_diagnostic.CA2016.severity = error + +# AV1555: Avoid using named arguments +# Disabled because it's common to use a named argument when passing `null` or bool arguments to make the parameter's purpose clear +dotnet_diagnostic.AV1555.severity = none + +#### Handling TODOs #### +# This is a popular rule in analyzers. Everyone has an opinion and +# some of the severity levels conflict. We don't need all of these +# to fire, only one. Pick one and mark it as informational so we +# don't lose track. +# S1135: Track uses of "TODO" tags +dotnet_diagnostic.S1135.severity = suggestion +# AV2318: Work-tracking TODO comment should be removed +dotnet_diagnostic.AV2318.severity = none +# MA0026: Fix TODO comment +dotnet_diagnostic.MA0026.severity = none \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..db25cda6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,23 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: "daily" + time: "08:00" + open-pull-requests-limit: 10 + - package-ecosystem: nuget + directory: "/" + schedule: + interval: "daily" + time: "08:30" + ignore: + # Microsoft.CodeAnalysis.* packages defined in the analyzer project can impact compatibility with older SDKs for + # our users. We don't want to bump these without first considering the user impact. + # + # We don't wildcard Microsoft.CodeAnalysis.* here though, as there are testing libraries and analyzers that + # can be upgraded without impacting our users. + - dependency-name: "Microsoft.CodeAnalysis.CSharp" + - dependency-name: "Microsoft.CodeAnalysis.CSharp.Workspaces" + - dependency-name: "Microsoft.CodeAnalysis.Common" + - dependency-name: "Microsoft.CodeAnalysis.Workspaces.Common" diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 00000000..c2e29f9b --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,36 @@ +# Add 'build' label to any change in the 'build' directory +build: +- changed-files: + - any-glob-to-any-file: 'build/**/*' + +# Add 'dependencies' label to any change in one of the packages files +dependencies: +- changed-files: + - any-glob-to-any-file: ['build/**/Packages.props', 'Directory.Packages.props'] + +# Add 'documentation' label to any change within the 'docs' directory or any '.md' file +documentation: +- changed-files: + - any-glob-to-any-file: ['docs/**', '**/*.md'] + +# Add 'feature' label to any PR where the head branch name starts with `feature` or has a `feature` section in the name +feature: + - head-branch: ['^feature', 'feature'] + +# Add 'bug' label to any PR where the head branch name starts with `bug` or has a `bug` section in the name +bug: + - head-branch: ['^bug', 'bug'] + +# Add 'releasable' label to any PR that is opened against the `main` branch +releasable: + - base-branch: 'main' + +# Add 'github_actions' label to any change to one of the GitHub workflows or configuration files +github_actions: +- changed-files: + - any-glob-to-any-file: ['.github/workflows/*.yml', '.github/dependabot.yml', '.github/labeler.yml'] + +# Add 'analyzers' label to any change to an analyzer, code fix, or shipping documentation +analyzers: +- changed-files: + - any-glob-to-any-file: ['src/Moq.Analyzers/AnalyzerReleases.*.md', 'src/Moq.Analyzers/**/*Analyzer.cs', 'src/Moq.Analyzers/**/*CodeFix.cs'] diff --git a/.github/workflows/label-issues.yml b/.github/workflows/label-issues.yml new file mode 100644 index 00000000..b4eb5292 --- /dev/null +++ b/.github/workflows/label-issues.yml @@ -0,0 +1,50 @@ +# This workflow will read information about an issue and attempt to label it initially for triage and sorting + +name: Label issues +on: + issues: + types: + - reopened + - opened + +jobs: + label_issues: + name: "Issue: add labels" + if: ${{ github.event.action == 'opened' || github.event.action == 'reopened' }} + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }} + script: | + // Get the issue body and title + const body = context.payload.issue.body + let title = context.payload.issue.title + + // Define the labels array + let labels = ["triage"] + + // Check if the body or the title contains the word 'PowerShell' (case-insensitive) + if ((body != null && body.match(/powershell/i)) || (title != null && title.match(/powershell/i))) { + // Add the 'powershell' label to the array + labels.push("powershell") + } + + // Check if the body or the title contains the words 'dotnet', '.net', 'c#' or 'csharp' (case-insensitive) + if ((body != null && body.match(/.net/i)) || (title != null && title.match(/.net/i)) || + (body != null && body.match(/dotnet/i)) || (title != null && title.match(/dotnet/i)) || + (body != null && body.match(/C#/i)) || (title != null && title.match(/C#/i)) || + (body != null && body.match(/csharp/i)) || (title != null && title.match(/csharp/i))) { + // Add the '.NET' label to the array + labels.push(".NET") + } + + // Add the labels to the issue + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: labels + }); \ No newline at end of file diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml new file mode 100644 index 00000000..a516e8b0 --- /dev/null +++ b/.github/workflows/label-pr.yml @@ -0,0 +1,21 @@ +# This workflow will triage pull requests and apply a label based on the +# paths that are modified in the pull request. +# +# To use this workflow, you will need to set up a .github/labeler.yml +# file with configuration. For more information, see: +# https://github.com/actions/labeler + +name: Label PR +on: [pull_request_target] + +jobs: + add_label: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - uses: actions/labeler@v5 + with: + repo-token: "${{ secrets.GH_ACTIONS_PR_WRITE }}" \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 699f7e16..014369a6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,6 +8,10 @@ on: push: branches: - main + merge_group: + branches: + - main + workflow_call: # Allow to be called from the release workflow schedule: - cron: '31 15 * * 0' # Run periodically to keep CodeQL database updated @@ -26,6 +30,9 @@ jobs: runs-on: ${{ matrix.os }} + env: + IS_COVERAGE_ALLOWED: ${{ secrets.CODACY_PROJECT_TOKEN != '' }} + steps: - uses: actions/checkout@v4 with: @@ -81,10 +88,17 @@ jobs: name: .NET Code Coverage Reports (${{ matrix.os }}) path: "artifacts/TestResults/coverage/**" - - name: Publish coverage summary + - name: Publish coverage summary to GitHub run: cat artifacts/TestResults/coverage/SummaryGithub.md >> $GITHUB_STEP_SUMMARY shell: bash + - name: Upload coverage data to Codacy + if: ${{ runner.os == 'Linux' && env.IS_COVERAGE_ALLOWED == 'true' }} + uses: codacy/codacy-coverage-reporter-action@v1.3.0 + with: + project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} + coverage-reports: ${{ github.workspace }}/artifacts/TestResults/coverage/Cobertura.xml + - name: Upload binlogs uses: actions/upload-artifact@v4 with: diff --git a/Directory.Packages.props b/Directory.Packages.props index 672bc235..f474f00b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,7 +20,8 @@ + - + \ No newline at end of file diff --git a/Moq.Analyzers.sln b/Moq.Analyzers.sln index 8d30b87d..4442c49b 100644 --- a/Moq.Analyzers.sln +++ b/Moq.Analyzers.sln @@ -3,9 +3,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.10.34928.147 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Analyzers", "Source\Moq.Analyzers\Moq.Analyzers.csproj", "{41ECC571-F586-460A-9BED-23528C8210C4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Analyzers", "src\Moq.Analyzers\Moq.Analyzers.csproj", "{41ECC571-F586-460A-9BED-23528C8210C4}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Analyzers.Test", "Source\Moq.Analyzers.Test\Moq.Analyzers.Test.csproj", "{D2348836-7129-4BE5-8AE6-D05FC8C28FC1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Analyzers.Test", "tests\Moq.Analyzers.Test\Moq.Analyzers.Test.csproj", "{D2348836-7129-4BE5-8AE6-D05FC8C28FC1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Analyzers.Benchmarks", "tests\Moq.Analyzers.Benchmarks\Moq.Analyzers.Benchmarks.csproj", "{11B3412F-456C-452E-94D2-B42D5C52F61C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -21,6 +23,10 @@ Global {D2348836-7129-4BE5-8AE6-D05FC8C28FC1}.Debug|Any CPU.Build.0 = Debug|Any CPU {D2348836-7129-4BE5-8AE6-D05FC8C28FC1}.Release|Any CPU.ActiveCfg = Release|Any CPU {D2348836-7129-4BE5-8AE6-D05FC8C28FC1}.Release|Any CPU.Build.0 = Release|Any CPU + {11B3412F-456C-452E-94D2-B42D5C52F61C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11B3412F-456C-452E-94D2-B42D5C52F61C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11B3412F-456C-452E-94D2-B42D5C52F61C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11B3412F-456C-452E-94D2-B42D5C52F61C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index 366e898b..3e8782eb 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ [![NuGet Version](https://img.shields.io/nuget/v/Moq.Analyzers?style=flat&logo=nuget&color=blue)](https://www.nuget.org/packages/Moq.Analyzers) [![NuGet Downloads](https://img.shields.io/nuget/dt/Moq.Analyzers?style=flat&logo=nuget)](https://www.nuget.org/packages/Moq.Analyzers) [![Main build](https://github.com/rjmurillo/moq.analyzers/actions/workflows/main.yml/badge.svg)](https://github.com/rjmurillo/moq.analyzers/actions/workflows/main.yml) +[![Codacy Grade Badge](https://app.codacy.com/project/badge/Grade/fc7c184dcb1843d4b1ae1b926fb82d5a)](https://app.codacy.com/gh/rjmurillo/moq.analyzers/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) +[![Codacy Coverage Badge](https://app.codacy.com/project/badge/Coverage/fc7c184dcb1843d4b1ae1b926fb82d5a)](https://app.codacy.com/gh/rjmurillo/moq.analyzers/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage) **Moq.Analyzers** is a Roslyn analyzer that helps you to write unit tests using the popular [Moq](https://github.com/devlooped/moq) framework. Moq.Analyzers protects you from common mistakes and warns you if diff --git a/Source/Moq.Analyzers.Test/PackageTests.cs b/Source/Moq.Analyzers.Test/PackageTests.cs deleted file mode 100644 index d2347c95..00000000 --- a/Source/Moq.Analyzers.Test/PackageTests.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Reflection; - -namespace Moq.Analyzers.Test; - -public class PackageTests -{ - private static readonly FileInfo Package; - - static PackageTests() - { - Package = new FileInfo(Assembly.GetExecutingAssembly().Location) - .Directory! - .GetFiles("Moq.Analyzers*.nupkg") - .OrderByDescending(f => f.LastWriteTimeUtc) - .First(); - } - - [Fact] - public Task Baseline() - { - return VerifyFile(Package).ScrubNuspec(); - } -} diff --git a/Source/Moq.Analyzers/Helpers.cs b/Source/Moq.Analyzers/Helpers.cs deleted file mode 100644 index ab0165c1..00000000 --- a/Source/Moq.Analyzers/Helpers.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System.Diagnostics; - -namespace Moq.Analyzers; - -internal static class Helpers -{ - private static readonly MoqMethodDescriptorBase MoqSetupMethodDescriptor = new MoqSetupMethodDescriptor(); - - internal static bool IsMoqSetupMethod(SemanticModel semanticModel, MemberAccessExpressionSyntax method, CancellationToken cancellationToken) - { - return MoqSetupMethodDescriptor.IsMatch(semanticModel, method, cancellationToken); - } - - internal static bool IsCallbackOrReturnInvocation(SemanticModel semanticModel, InvocationExpressionSyntax callbackOrReturnsInvocation) - { - MemberAccessExpressionSyntax? callbackOrReturnsMethod = callbackOrReturnsInvocation.Expression as MemberAccessExpressionSyntax; - - Debug.Assert(callbackOrReturnsMethod != null, nameof(callbackOrReturnsMethod) + " != null"); - - if (callbackOrReturnsMethod == null) - { - return false; - } - - string? methodName = callbackOrReturnsMethod.Name.ToString(); - - // First fast check before walking semantic model - if (!string.Equals(methodName, "Callback", StringComparison.Ordinal) - && !string.Equals(methodName, "Returns", StringComparison.Ordinal)) - { - return false; - } - - SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(callbackOrReturnsMethod); - return symbolInfo.CandidateReason switch - { - CandidateReason.OverloadResolutionFailure => symbolInfo.CandidateSymbols.Any(IsCallbackOrReturnSymbol), - CandidateReason.None => IsCallbackOrReturnSymbol(symbolInfo.Symbol), - _ => false, - }; - } - - internal static InvocationExpressionSyntax? FindSetupMethodFromCallbackInvocation(SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken) - { - InvocationExpressionSyntax? invocation = expression as InvocationExpressionSyntax; - if (invocation?.Expression is not MemberAccessExpressionSyntax method) return null; - if (IsMoqSetupMethod(semanticModel, method, cancellationToken)) return invocation; - return FindSetupMethodFromCallbackInvocation(semanticModel, method.Expression, cancellationToken); - } - - internal static InvocationExpressionSyntax? FindMockedMethodInvocationFromSetupMethod(InvocationExpressionSyntax? setupInvocation) - { - LambdaExpressionSyntax? setupLambdaArgument = setupInvocation?.ArgumentList.Arguments[0].Expression as LambdaExpressionSyntax; - return setupLambdaArgument?.Body as InvocationExpressionSyntax; - } - - internal static ExpressionSyntax? FindMockedMemberExpressionFromSetupMethod(InvocationExpressionSyntax? setupInvocation) - { - LambdaExpressionSyntax? setupLambdaArgument = setupInvocation?.ArgumentList.Arguments[0].Expression as LambdaExpressionSyntax; - return setupLambdaArgument?.Body as ExpressionSyntax; - } - - internal static IEnumerable GetAllMatchingMockedMethodSymbolsFromSetupMethodInvocation(SemanticModel semanticModel, InvocationExpressionSyntax? setupMethodInvocation) - { - LambdaExpressionSyntax? setupLambdaArgument = setupMethodInvocation?.ArgumentList.Arguments[0].Expression as LambdaExpressionSyntax; - InvocationExpressionSyntax? mockedMethodInvocation = setupLambdaArgument?.Body as InvocationExpressionSyntax; - - return GetAllMatchingSymbols(semanticModel, mockedMethodInvocation); - } - - internal static IEnumerable GetAllMatchingSymbols(SemanticModel semanticModel, ExpressionSyntax? expression) - where T : class - { - List? matchingSymbols = new List(); - if (expression != null) - { - SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(expression); - if (symbolInfo is { CandidateReason: CandidateReason.None, Symbol: T }) - { - matchingSymbols.Add(symbolInfo.Symbol as T); - } - else if (symbolInfo.CandidateReason == CandidateReason.OverloadResolutionFailure) - { - matchingSymbols.AddRange(symbolInfo.CandidateSymbols.OfType()); - } - } - - return matchingSymbols; - } - - private static bool IsCallbackOrReturnSymbol(ISymbol? symbol) - { - // TODO: Check what is the best way to do such checks - if (symbol is not IMethodSymbol methodSymbol) return false; - string? methodName = methodSymbol.ToString(); - return methodName.StartsWith("Moq.Language.ICallback", StringComparison.Ordinal) - || methodName.StartsWith("Moq.Language.IReturns", StringComparison.Ordinal); - } -} diff --git a/build/targets/codeanalysis/.globalconfig b/build/targets/codeanalysis/.globalconfig new file mode 100644 index 00000000..0b6e8efa --- /dev/null +++ b/build/targets/codeanalysis/.globalconfig @@ -0,0 +1,9 @@ +is_global=true + +# Prefer configuring diagnostics in .editorconfig over this .globalconfig because it is understood by more editors. +# Only use this file for configuring diagnostics that aren't tied to a source file, and thus can't be placed under +# any .editorconfig section. + +# AV2210 : Pass -warnaserror to the compiler or add True to your project file +# This is set as part of the CI build. It is intentionally not set locally to allow for a fast inner dev loop. +dotnet_diagnostic.AV2210.severity = none diff --git a/build/targets/codeanalysis/CodeAnalysis.props b/build/targets/codeanalysis/CodeAnalysis.props index d49d75d9..a66e6a33 100644 --- a/build/targets/codeanalysis/CodeAnalysis.props +++ b/build/targets/codeanalysis/CodeAnalysis.props @@ -5,10 +5,13 @@ preview 9999 true + true + true - + + @@ -25,5 +28,18 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/build/targets/codeanalysis/Packages.props b/build/targets/codeanalysis/Packages.props index 19c793af..a2a0d873 100644 --- a/build/targets/codeanalysis/Packages.props +++ b/build/targets/codeanalysis/Packages.props @@ -1,8 +1,12 @@ - + + + + + diff --git a/Source/stylecop.json b/build/targets/codeanalysis/stylecop.json similarity index 100% rename from Source/stylecop.json rename to build/targets/codeanalysis/stylecop.json diff --git a/build/targets/tests/Packages.props b/build/targets/tests/Packages.props index fc5e9f1d..e5c094e6 100644 --- a/build/targets/tests/Packages.props +++ b/build/targets/tests/Packages.props @@ -1,11 +1,10 @@ - - + - + diff --git a/build/targets/tests/Tests.props b/build/targets/tests/Tests.props index 9a32918b..e26d218c 100644 --- a/build/targets/tests/Tests.props +++ b/build/targets/tests/Tests.props @@ -8,5 +8,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/docs/rules/README.md b/docs/rules/README.md index e6be3865..ab77dfc1 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -2,11 +2,11 @@ | ID | Title | | --- | --- | -[Moq1000](./Moq1000.md) | Sealed classes cannot be mocked -[Moq1001](./Moq1001.md) | Mocked interfaces cannot have constructor parameters -[Moq1002](./Moq1002.md) | Parameters provided into mock do not match any existing constructors -[Moq1100](./Moq1100.md) | Callback signature must match the signature of the mocked method -[Moq1101](./Moq1101.md) | SetupGet/SetupSet should be used for properties, not for methods -[Moq1200](./Moq1200.md) | Setup should be used only for overridable members -[Moq1201](./Moq1201.md) | Setup of async methods should use `.ReturnsAsync` instance instead of `.Result` -[Moq1300](./Moq1300.md) | `Mock.As()` should take interfaces only +| [Moq1000](./Moq1000.md) | Sealed classes cannot be mocked | +| [Moq1001](./Moq1001.md) | Mocked interfaces cannot have constructor parameters | +| [Moq1002](./Moq1002.md) | Parameters provided into mock do not match any existing constructors | +| [Moq1100](./Moq1100.md) | Callback signature must match the signature of the mocked method | +| [Moq1101](./Moq1101.md) | SetupGet/SetupSet should be used for properties, not for methods | +| [Moq1200](./Moq1200.md) | Setup should be used only for overridable members | +| [Moq1201](./Moq1201.md) | Setup of async methods should use `.ReturnsAsync` instance instead of `.Result` | +| [Moq1300](./Moq1300.md) | `Mock.As()` should take interfaces only | diff --git a/Source/Moq.Analyzers/AnalyzerReleases.Shipped.md b/src/Moq.Analyzers/AnalyzerReleases.Shipped.md similarity index 100% rename from Source/Moq.Analyzers/AnalyzerReleases.Shipped.md rename to src/Moq.Analyzers/AnalyzerReleases.Shipped.md diff --git a/Source/Moq.Analyzers/AnalyzerReleases.Unshipped.md b/src/Moq.Analyzers/AnalyzerReleases.Unshipped.md similarity index 100% rename from Source/Moq.Analyzers/AnalyzerReleases.Unshipped.md rename to src/Moq.Analyzers/AnalyzerReleases.Unshipped.md diff --git a/Source/Moq.Analyzers/AsShouldBeUsedOnlyForInterfaceAnalyzer.cs b/src/Moq.Analyzers/AsShouldBeUsedOnlyForInterfaceAnalyzer.cs similarity index 91% rename from Source/Moq.Analyzers/AsShouldBeUsedOnlyForInterfaceAnalyzer.cs rename to src/Moq.Analyzers/AsShouldBeUsedOnlyForInterfaceAnalyzer.cs index 8ee18fb4..887c9a78 100644 --- a/Source/Moq.Analyzers/AsShouldBeUsedOnlyForInterfaceAnalyzer.cs +++ b/src/Moq.Analyzers/AsShouldBeUsedOnlyForInterfaceAnalyzer.cs @@ -32,6 +32,7 @@ public override void Initialize(AnalysisContext context) context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "AV1500:Member or local function contains too many statements", Justification = "Tracked in https://github.com/rjmurillo/moq.analyzers/issues/90")] private static void Analyze(SyntaxNodeAnalysisContext context) { if (context.Node is not InvocationExpressionSyntax invocationExpression) diff --git a/Source/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodAnalyzer.cs b/src/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodAnalyzer.cs similarity index 66% rename from Source/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodAnalyzer.cs rename to src/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodAnalyzer.cs index e9bc2d96..8760a9d2 100644 --- a/Source/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodAnalyzer.cs +++ b/src/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodAnalyzer.cs @@ -1,3 +1,5 @@ +using System.Diagnostics; + namespace Moq.Analyzers; /// @@ -33,16 +35,17 @@ public override void Initialize(AnalysisContext context) context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "AV1500:Member or local function contains too many statements", Justification = "Tracked in https://github.com/rjmurillo/moq.analyzers/issues/90")] private static void Analyze(SyntaxNodeAnalysisContext context) { - InvocationExpressionSyntax? callbackOrReturnsInvocation = (InvocationExpressionSyntax)context.Node; + InvocationExpressionSyntax callbackOrReturnsInvocation = (InvocationExpressionSyntax)context.Node; SeparatedSyntaxList callbackOrReturnsMethodArguments = callbackOrReturnsInvocation.ArgumentList.Arguments; // Ignoring Callback() and Return() calls without lambda arguments if (callbackOrReturnsMethodArguments.Count == 0) return; - if (!Helpers.IsCallbackOrReturnInvocation(context.SemanticModel, callbackOrReturnsInvocation)) return; + if (!context.SemanticModel.IsCallbackOrReturnInvocation(callbackOrReturnsInvocation)) return; ParenthesizedLambdaExpressionSyntax? callbackLambda = callbackOrReturnsInvocation.ArgumentList.Arguments[0]?.Expression as ParenthesizedLambdaExpressionSyntax; @@ -53,28 +56,36 @@ private static void Analyze(SyntaxNodeAnalysisContext context) SeparatedSyntaxList lambdaParameters = callbackLambda.ParameterList.Parameters; if (lambdaParameters.Count == 0) return; - InvocationExpressionSyntax? setupInvocation = Helpers.FindSetupMethodFromCallbackInvocation(context.SemanticModel, callbackOrReturnsInvocation, context.CancellationToken); - InvocationExpressionSyntax? mockedMethodInvocation = Helpers.FindMockedMethodInvocationFromSetupMethod(setupInvocation); + InvocationExpressionSyntax? setupInvocation = context.SemanticModel.FindSetupMethodFromCallbackInvocation(callbackOrReturnsInvocation, context.CancellationToken); + InvocationExpressionSyntax? mockedMethodInvocation = setupInvocation.FindMockedMethodInvocationFromSetupMethod(); if (mockedMethodInvocation == null) return; SeparatedSyntaxList mockedMethodArguments = mockedMethodInvocation.ArgumentList.Arguments; if (mockedMethodArguments.Count != lambdaParameters.Count) { - Diagnostic? diagnostic = Diagnostic.Create(Rule, callbackLambda.ParameterList.GetLocation()); + Diagnostic diagnostic = Diagnostic.Create(Rule, callbackLambda.ParameterList.GetLocation()); context.ReportDiagnostic(diagnostic); } else { - for (int i = 0; i < mockedMethodArguments.Count; i++) + for (int argumentIndex = 0; argumentIndex < mockedMethodArguments.Count; argumentIndex++) { - TypeInfo mockedMethodArgumentType = context.SemanticModel.GetTypeInfo(mockedMethodArguments[i].Expression, context.CancellationToken); - TypeInfo lambdaParameterType = context.SemanticModel.GetTypeInfo(lambdaParameters[i].Type, context.CancellationToken); + TypeSyntax? lambdaParameterTypeSyntax = lambdaParameters[argumentIndex].Type; + + // TODO: Don't know if continue or break is the right thing to do here + if (lambdaParameterTypeSyntax is null) continue; + + TypeInfo lambdaParameterType = context.SemanticModel.GetTypeInfo(lambdaParameterTypeSyntax, context.CancellationToken); + + TypeInfo mockedMethodArgumentType = context.SemanticModel.GetTypeInfo(mockedMethodArguments[argumentIndex].Expression, context.CancellationToken); + string? mockedMethodTypeName = mockedMethodArgumentType.ConvertedType?.ToString(); string? lambdaParameterTypeName = lambdaParameterType.ConvertedType?.ToString(); + if (!string.Equals(mockedMethodTypeName, lambdaParameterTypeName, StringComparison.Ordinal)) { - Diagnostic? diagnostic = Diagnostic.Create(Rule, callbackLambda.ParameterList.GetLocation()); + Diagnostic diagnostic = Diagnostic.Create(Rule, callbackLambda.ParameterList.GetLocation()); context.ReportDiagnostic(diagnostic); } } diff --git a/Source/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodCodeFix.cs b/src/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodCodeFix.cs similarity index 66% rename from Source/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodCodeFix.cs rename to src/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodCodeFix.cs index 87f403a6..70a6eed7 100644 --- a/Source/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodCodeFix.cs +++ b/src/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodCodeFix.cs @@ -35,7 +35,7 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) return; } - Diagnostic? diagnostic = context.Diagnostics.First(); + Diagnostic diagnostic = context.Diagnostics.First(); TextSpan diagnosticSpan = diagnostic.Location.SourceSpan; // Find the type declaration identified by the diagnostic. @@ -48,18 +48,17 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) // Register a code action that will invoke the fix. context.RegisterCodeFix( CodeAction.Create( - title: "Fix Moq callback signature", - createChangedDocument: c => FixCallbackSignatureAsync(root, context.Document, badArgumentListSyntax, c), - equivalenceKey: "Fix Moq callback signature"), + "Fix Moq callback signature", + cancellationToken => FixCallbackSignatureAsync(root, context.Document, badArgumentListSyntax, cancellationToken), + "Fix Moq callback signature"), diagnostic); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "AV1500:Member or local function contains too many statements", Justification = "Tracked in https://github.com/rjmurillo/moq.analyzers/issues/90")] private async Task FixCallbackSignatureAsync(SyntaxNode root, Document document, ParameterListSyntax? oldParameters, CancellationToken cancellationToken) { SemanticModel? semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - Debug.Assert(semanticModel != null, nameof(semanticModel) + " != null"); - if (semanticModel == null) { return document; @@ -70,23 +69,23 @@ private async Task FixCallbackSignatureAsync(SyntaxNode root, Document return document; } - InvocationExpressionSyntax? setupMethodInvocation = Helpers.FindSetupMethodFromCallbackInvocation(semanticModel, callbackInvocation, cancellationToken); + InvocationExpressionSyntax? setupMethodInvocation = semanticModel.FindSetupMethodFromCallbackInvocation(callbackInvocation, cancellationToken); Debug.Assert(setupMethodInvocation != null, nameof(setupMethodInvocation) + " != null"); - IMethodSymbol[]? matchingMockedMethods = Helpers.GetAllMatchingMockedMethodSymbolsFromSetupMethodInvocation(semanticModel, setupMethodInvocation).ToArray(); + IMethodSymbol[] matchingMockedMethods = semanticModel.GetAllMatchingMockedMethodSymbolsFromSetupMethodInvocation(setupMethodInvocation).ToArray(); - if (matchingMockedMethods.Length != 1 || oldParameters == null) + if (matchingMockedMethods.Length != 1) { return document; } - ParameterListSyntax? newParameters = SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList(matchingMockedMethods[0].Parameters.Select( - p => + ParameterListSyntax newParameters = SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList(matchingMockedMethods[0].Parameters.Select( + parameterSymbol => { - TypeSyntax? type = SyntaxFactory.ParseTypeName(p.Type.ToMinimalDisplayString(semanticModel, oldParameters.SpanStart)); - return SyntaxFactory.Parameter(default, SyntaxFactory.TokenList(), type, SyntaxFactory.Identifier(p.Name), null); + TypeSyntax type = SyntaxFactory.ParseTypeName(parameterSymbol.Type.ToMinimalDisplayString(semanticModel, oldParameters.SpanStart)); + return SyntaxFactory.Parameter(default, SyntaxFactory.TokenList(), type, SyntaxFactory.Identifier(parameterSymbol.Name), null); }))); - SyntaxNode? newRoot = root.ReplaceNode(oldParameters, newParameters); + SyntaxNode newRoot = root.ReplaceNode(oldParameters, newParameters); return document.WithSyntaxRoot(newRoot); } } diff --git a/Source/Moq.Analyzers/ConstructorArgumentsShouldMatchAnalyzer.cs b/src/Moq.Analyzers/ConstructorArgumentsShouldMatchAnalyzer.cs similarity index 54% rename from Source/Moq.Analyzers/ConstructorArgumentsShouldMatchAnalyzer.cs rename to src/Moq.Analyzers/ConstructorArgumentsShouldMatchAnalyzer.cs index b08d5c99..69922915 100644 --- a/Source/Moq.Analyzers/ConstructorArgumentsShouldMatchAnalyzer.cs +++ b/src/Moq.Analyzers/ConstructorArgumentsShouldMatchAnalyzer.cs @@ -35,77 +35,105 @@ public override void Initialize(AnalysisContext context) context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.ObjectCreationExpression); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "AV1500:Member or local function contains too many statements", Justification = "Tracked in https://github.com/rjmurillo/moq.analyzers/issues/90")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "MA0051:Method is too long", Justification = "Tracked in #90")] private static void Analyze(SyntaxNodeAnalysisContext context) { - ObjectCreationExpressionSyntax? objectCreation = (ObjectCreationExpressionSyntax)context.Node; + ObjectCreationExpressionSyntax objectCreation = (ObjectCreationExpressionSyntax)context.Node; GenericNameSyntax? genericName = GetGenericNameSyntax(objectCreation.Type); if (genericName == null) return; - if (!IsMockGenericType(genericName)) return; + if (!IsMockGenericType(genericName)) + { + return; + } // Full check that we are calling new Mock() IMethodSymbol? constructorSymbol = GetConstructorSymbol(context, objectCreation); + // If constructorSymbol is null, we should have caught that earlier (and we cannot proceed) + if (constructorSymbol == null) + { + return; + } + // Vararg parameter is the one that takes all arguments for mocked class constructor - IParameterSymbol? varArgsConstructorParameter = constructorSymbol?.Parameters.FirstOrDefault(x => x.IsParams); + IParameterSymbol? varArgsConstructorParameter = constructorSymbol.Parameters.FirstOrDefault(parameterSymbol => parameterSymbol.IsParams); // Vararg parameter are not used, so there are no arguments for mocked class constructor - if (varArgsConstructorParameter == null) return; - - Debug.Assert(constructorSymbol != null, nameof(constructorSymbol) + " != null"); - - if (constructorSymbol == null) + if (varArgsConstructorParameter == null) { return; } - int varArgsConstructorParameterIdx = constructorSymbol.Parameters.IndexOf(varArgsConstructorParameter); + int varArgsConstructorParameterIndex = constructorSymbol.Parameters.IndexOf(varArgsConstructorParameter); // Find mocked type INamedTypeSymbol? mockedTypeSymbol = GetMockedSymbol(context, genericName); - if (mockedTypeSymbol == null) return; + if (mockedTypeSymbol == null) + { + return; + } // Skip first argument if it is not vararg - typically it is MockingBehavior argument - ArgumentSyntax[]? constructorArguments = objectCreation.ArgumentList?.Arguments.Skip(varArgsConstructorParameterIdx == 0 ? 0 : 1).ToArray(); + IEnumerable? constructorArguments = objectCreation.ArgumentList?.Arguments.Skip(varArgsConstructorParameterIndex == 0 ? 0 : 1); if (!mockedTypeSymbol.IsAbstract) { - if (constructorArguments != null - && IsConstructorMismatch(context, objectCreation, genericName, constructorArguments) - && objectCreation.ArgumentList != null) - { - Diagnostic? diagnostic = Diagnostic.Create(Rule, objectCreation.ArgumentList.GetLocation()); - context.ReportDiagnostic(diagnostic); - } + AnalyzeConcrete(context, constructorArguments, objectCreation, genericName); } else { // Issue #1: Currently detection does not work well for abstract classes because they cannot be instantiated // The mocked symbol is abstract, so we need to check if the constructor arguments match the abstract class constructor + AnalyzeAbstract(context, constructorArguments, mockedTypeSymbol, objectCreation); + } + } - // Extract types of arguments passed in the constructor call - if (constructorArguments != null) - { - ITypeSymbol[] argumentTypes = constructorArguments - .Select(arg => context.SemanticModel.GetTypeInfo(arg.Expression, context.CancellationToken).Type) - .ToArray()!; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "AV1500:Member or local function contains too many statements", Justification = "Tracked in https://github.com/rjmurillo/moq.analyzers/issues/90")] + private static void AnalyzeAbstract( + SyntaxNodeAnalysisContext context, + IEnumerable? constructorArguments, + INamedTypeSymbol mockedTypeSymbol, + ObjectCreationExpressionSyntax objectCreation) + { + // Extract types of arguments passed in the constructor call + if (constructorArguments != null) + { + ITypeSymbol[] argumentTypes = constructorArguments + .Select(arg => context.SemanticModel.GetTypeInfo(arg.Expression, context.CancellationToken).Type) + .ToArray()!; - // Check all constructors of the abstract type - for (int i = 0; i < mockedTypeSymbol.Constructors.Length; i++) + // Check all constructors of the abstract type + for (int constructorIndex = 0; constructorIndex < mockedTypeSymbol.Constructors.Length; constructorIndex++) + { + IMethodSymbol constructor = mockedTypeSymbol.Constructors[constructorIndex]; + if (AreParametersMatching(constructor.Parameters, argumentTypes)) { - IMethodSymbol constructor = mockedTypeSymbol.Constructors[i]; - if (AreParametersMatching(constructor.Parameters, argumentTypes)) - { - return; // Found a matching constructor - } + return; } } + } - Debug.Assert(objectCreation.ArgumentList != null, "objectCreation.ArgumentList != null"); + Debug.Assert(objectCreation.ArgumentList != null, "objectCreation.ArgumentList != null"); - Diagnostic? diagnostic = Diagnostic.Create(Rule, objectCreation.ArgumentList?.GetLocation()); + Diagnostic diagnostic = Diagnostic.Create(Rule, objectCreation.ArgumentList?.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + + private static void AnalyzeConcrete( + SyntaxNodeAnalysisContext context, + IEnumerable? constructorArguments, + ObjectCreationExpressionSyntax objectCreation, + GenericNameSyntax genericName) + { + if (constructorArguments != null + && IsConstructorMismatch(context, objectCreation, genericName, constructorArguments) + && objectCreation.ArgumentList != null) + { + Diagnostic diagnostic = Diagnostic.Create(Rule, objectCreation.ArgumentList.GetLocation()); context.ReportDiagnostic(diagnostic); } } @@ -121,18 +149,20 @@ private static void Analyze(SyntaxNodeAnalysisContext context) return mockedTypeSymbol; } - private static bool AreParametersMatching(ImmutableArray constructorParameters, ITypeSymbol[] argumentTypes2) + private static bool AreParametersMatching( + ImmutableArray constructorParameters, + ITypeSymbol[] argumentTypes) { // Check if the number of parameters matches - if (constructorParameters.Length != argumentTypes2.Length) + if (constructorParameters.Length != argumentTypes.Length) { return false; } // Check if each parameter type matches in order - for (int i = 0; i < constructorParameters.Length; i++) + for (int constructorParameterIndex = 0; constructorParameterIndex < constructorParameters.Length; constructorParameterIndex++) { - if (!constructorParameters[i].Type.Equals(argumentTypes2[i], SymbolEqualityComparer.IncludeNullability)) + if (!constructorParameters[constructorParameterIndex].Type.Equals(argumentTypes[constructorParameterIndex], SymbolEqualityComparer.IncludeNullability)) { return false; } @@ -143,6 +173,8 @@ private static bool AreParametersMatching(ImmutableArray const private static GenericNameSyntax? GetGenericNameSyntax(TypeSyntax typeSyntax) { + // REVIEW: Switch and ifs are equal in this case, but switch causes AV1535 to trigger + // The switch expression adds more instructions to do the same, so stick with ifs if (typeSyntax is GenericNameSyntax genericNameSyntax) { return genericNameSyntax; @@ -166,6 +198,7 @@ private static bool IsMockGenericType(GenericNameSyntax genericName) { SymbolInfo constructorSymbolInfo = context.SemanticModel.GetSymbolInfo(objectCreation, context.CancellationToken); IMethodSymbol? constructorSymbol = constructorSymbolInfo.Symbol as IMethodSymbol; + return constructorSymbol?.MethodKind == MethodKind.Constructor && string.Equals( constructorSymbol.ContainingType?.ConstructedFrom.ToDisplayString(), @@ -175,12 +208,16 @@ private static bool IsMockGenericType(GenericNameSyntax genericName) : null; } - private static bool IsConstructorMismatch(SyntaxNodeAnalysisContext context, ObjectCreationExpressionSyntax objectCreation, GenericNameSyntax genericName, ArgumentSyntax[] constructorArguments) + private static bool IsConstructorMismatch( + SyntaxNodeAnalysisContext context, + ObjectCreationExpressionSyntax objectCreation, + GenericNameSyntax genericName, + IEnumerable constructorArguments) { - ObjectCreationExpressionSyntax? fakeConstructorCall = SyntaxFactory.ObjectCreationExpression( + ObjectCreationExpressionSyntax fakeConstructorCall = SyntaxFactory.ObjectCreationExpression( genericName.TypeArgumentList.Arguments.First(), SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(constructorArguments)), - null); + initializer: null); SymbolInfo mockedClassConstructorSymbolInfo = context.SemanticModel.GetSpeculativeSymbolInfo( objectCreation.SpanStart, fakeConstructorCall, SpeculativeBindingOption.BindAsExpression); diff --git a/Source/Moq.Analyzers/DiagnosticCategory.cs b/src/Moq.Analyzers/DiagnosticCategory.cs similarity index 100% rename from Source/Moq.Analyzers/DiagnosticCategory.cs rename to src/Moq.Analyzers/DiagnosticCategory.cs diff --git a/Source/Moq.Analyzers.Test/GlobalSuppressions.cs b/src/Moq.Analyzers/GlobalSuppressions.cs similarity index 100% rename from Source/Moq.Analyzers.Test/GlobalSuppressions.cs rename to src/Moq.Analyzers/GlobalSuppressions.cs diff --git a/Source/Moq.Analyzers/GlobalUsings.cs b/src/Moq.Analyzers/GlobalUsings.cs similarity index 100% rename from Source/Moq.Analyzers/GlobalUsings.cs rename to src/Moq.Analyzers/GlobalUsings.cs diff --git a/src/Moq.Analyzers/InvocationExpressionSyntaxExtensions.cs b/src/Moq.Analyzers/InvocationExpressionSyntaxExtensions.cs new file mode 100644 index 00000000..547074f7 --- /dev/null +++ b/src/Moq.Analyzers/InvocationExpressionSyntaxExtensions.cs @@ -0,0 +1,19 @@ +namespace Moq.Analyzers; + +/// +/// Extension methods for s. +/// +internal static class InvocationExpressionSyntaxExtensions +{ + internal static InvocationExpressionSyntax? FindMockedMethodInvocationFromSetupMethod(this InvocationExpressionSyntax? setupInvocation) + { + LambdaExpressionSyntax? setupLambdaArgument = setupInvocation?.ArgumentList.Arguments[0].Expression as LambdaExpressionSyntax; + return setupLambdaArgument?.Body as InvocationExpressionSyntax; + } + + internal static ExpressionSyntax? FindMockedMemberExpressionFromSetupMethod(this InvocationExpressionSyntax? setupInvocation) + { + LambdaExpressionSyntax? setupLambdaArgument = setupInvocation?.ArgumentList.Arguments[0].Expression as LambdaExpressionSyntax; + return setupLambdaArgument?.Body as ExpressionSyntax; + } +} diff --git a/Source/Moq.Analyzers/Moq.Analyzers.csproj b/src/Moq.Analyzers/Moq.Analyzers.csproj similarity index 100% rename from Source/Moq.Analyzers/Moq.Analyzers.csproj rename to src/Moq.Analyzers/Moq.Analyzers.csproj diff --git a/Source/Moq.Analyzers/MoqAsMethodDescriptor.cs b/src/Moq.Analyzers/MoqAsMethodDescriptor.cs similarity index 82% rename from Source/Moq.Analyzers/MoqAsMethodDescriptor.cs rename to src/Moq.Analyzers/MoqAsMethodDescriptor.cs index afe70d77..46265a13 100644 --- a/Source/Moq.Analyzers/MoqAsMethodDescriptor.cs +++ b/src/Moq.Analyzers/MoqAsMethodDescriptor.cs @@ -8,6 +8,7 @@ internal class MoqAsMethodDescriptor : MoqMethodDescriptorBase { private const string MethodName = "As"; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "AV1500:Member or local function contains too many statements", Justification = "Tracked in https://github.com/rjmurillo/moq.analyzers/issues/90")] public override bool IsMatch(SemanticModel semanticModel, MemberAccessExpressionSyntax memberAccessSyntax, CancellationToken cancellationToken) { if (!IsFastMatch(memberAccessSyntax, MethodName.AsSpan())) diff --git a/Source/Moq.Analyzers/MoqMethodDescriptorBase.cs b/src/Moq.Analyzers/MoqMethodDescriptorBase.cs similarity index 100% rename from Source/Moq.Analyzers/MoqMethodDescriptorBase.cs rename to src/Moq.Analyzers/MoqMethodDescriptorBase.cs diff --git a/Source/Moq.Analyzers/MoqSetupMethodDescriptor.cs b/src/Moq.Analyzers/MoqSetupMethodDescriptor.cs similarity index 82% rename from Source/Moq.Analyzers/MoqSetupMethodDescriptor.cs rename to src/Moq.Analyzers/MoqSetupMethodDescriptor.cs index fe42697b..9ca4f1ad 100644 --- a/Source/Moq.Analyzers/MoqSetupMethodDescriptor.cs +++ b/src/Moq.Analyzers/MoqSetupMethodDescriptor.cs @@ -8,6 +8,7 @@ internal class MoqSetupMethodDescriptor : MoqMethodDescriptorBase { private const string MethodName = "Setup"; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "AV1500:Member or local function contains too many statements", Justification = "Tracked in https://github.com/rjmurillo/moq.analyzers/issues/90")] public override bool IsMatch(SemanticModel semanticModel, MemberAccessExpressionSyntax memberAccessSyntax, CancellationToken cancellationToken) { if (!IsFastMatch(memberAccessSyntax, MethodName.AsSpan())) diff --git a/Source/Moq.Analyzers/SyntaxExtensions.cs b/src/Moq.Analyzers/NameSyntaxExtensions.cs similarity index 92% rename from Source/Moq.Analyzers/SyntaxExtensions.cs rename to src/Moq.Analyzers/NameSyntaxExtensions.cs index 9099f12e..45a5004a 100644 --- a/Source/Moq.Analyzers/SyntaxExtensions.cs +++ b/src/Moq.Analyzers/NameSyntaxExtensions.cs @@ -3,9 +3,9 @@ namespace Moq.Analyzers; /// -/// Extensions methods for s. +/// Extensions methods for s. /// -internal static class SyntaxExtensions +internal static class NameSyntaxExtensions { /// /// Tries to get the generic arguments of a given . diff --git a/Source/Moq.Analyzers/NoConstructorArgumentsForInterfaceMockAnalyzer.cs b/src/Moq.Analyzers/NoConstructorArgumentsForInterfaceMockAnalyzer.cs similarity index 79% rename from Source/Moq.Analyzers/NoConstructorArgumentsForInterfaceMockAnalyzer.cs rename to src/Moq.Analyzers/NoConstructorArgumentsForInterfaceMockAnalyzer.cs index 3a037f89..775cbf1d 100644 --- a/Source/Moq.Analyzers/NoConstructorArgumentsForInterfaceMockAnalyzer.cs +++ b/src/Moq.Analyzers/NoConstructorArgumentsForInterfaceMockAnalyzer.cs @@ -35,9 +35,10 @@ public override void Initialize(AnalysisContext context) context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.ObjectCreationExpression); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "AV1500:Member or local function contains too many statements", Justification = "Tracked in https://github.com/rjmurillo/moq.analyzers/issues/90")] private static void Analyze(SyntaxNodeAnalysisContext context) { - ObjectCreationExpressionSyntax? objectCreation = (ObjectCreationExpressionSyntax)context.Node; + ObjectCreationExpressionSyntax objectCreation = (ObjectCreationExpressionSyntax)context.Node; // TODO Think how to make this piece more elegant while fast GenericNameSyntax? genericName = objectCreation.Type as GenericNameSyntax; @@ -53,7 +54,13 @@ private static void Analyze(SyntaxNodeAnalysisContext context) // Full check SymbolInfo constructorSymbolInfo = context.SemanticModel.GetSymbolInfo(objectCreation, context.CancellationToken); - if (constructorSymbolInfo.Symbol is not IMethodSymbol constructorSymbol || constructorSymbol.ContainingType == null || constructorSymbol.ContainingType.ConstructedFrom == null) return; + if (constructorSymbolInfo.Symbol is not IMethodSymbol constructorSymbol + || constructorSymbol.ContainingType == null + || constructorSymbol.ContainingType.ConstructedFrom == null) + { + return; + } + if (constructorSymbol.MethodKind != MethodKind.Constructor) return; if (!string.Equals( constructorSymbol.ContainingType.ConstructedFrom.ToDisplayString(), @@ -63,8 +70,8 @@ private static void Analyze(SyntaxNodeAnalysisContext context) return; } - if (constructorSymbol.Parameters == null || constructorSymbol.Parameters.Length == 0) return; - if (!constructorSymbol.Parameters.Any(x => x.IsParams)) return; + if (constructorSymbol.Parameters.Length == 0) return; + if (!constructorSymbol.Parameters.Any(parameterSymbol => parameterSymbol.IsParams)) return; // Find mocked type SeparatedSyntaxList typeArguments = genericName.TypeArgumentList.Arguments; @@ -77,7 +84,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context) { Debug.Assert(objectCreation.ArgumentList != null, "objectCreation.ArgumentList != null"); - Diagnostic? diagnostic = Diagnostic.Create(Rule, objectCreation.ArgumentList?.GetLocation()); + Diagnostic diagnostic = Diagnostic.Create(Rule, objectCreation.ArgumentList?.GetLocation()); context.ReportDiagnostic(diagnostic); } } diff --git a/Source/Moq.Analyzers/NoMethodsInPropertySetupAnalyzer.cs b/src/Moq.Analyzers/NoMethodsInPropertySetupAnalyzer.cs similarity index 78% rename from Source/Moq.Analyzers/NoMethodsInPropertySetupAnalyzer.cs rename to src/Moq.Analyzers/NoMethodsInPropertySetupAnalyzer.cs index cd5a59ec..0880481e 100644 --- a/Source/Moq.Analyzers/NoMethodsInPropertySetupAnalyzer.cs +++ b/src/Moq.Analyzers/NoMethodsInPropertySetupAnalyzer.cs @@ -33,21 +33,22 @@ public override void Initialize(AnalysisContext context) context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "AV1500:Member or local function contains too many statements", Justification = "Tracked in https://github.com/rjmurillo/moq.analyzers/issues/90")] private static void Analyze(SyntaxNodeAnalysisContext context) { - InvocationExpressionSyntax? setupGetOrSetInvocation = (InvocationExpressionSyntax)context.Node; + InvocationExpressionSyntax setupGetOrSetInvocation = (InvocationExpressionSyntax)context.Node; if (setupGetOrSetInvocation.Expression is not MemberAccessExpressionSyntax setupGetOrSetMethod) return; if (!string.Equals(setupGetOrSetMethod.Name.ToFullString(), "SetupGet", StringComparison.Ordinal) && !string.Equals(setupGetOrSetMethod.Name.ToFullString(), "SetupSet", StringComparison.Ordinal)) return; - InvocationExpressionSyntax? mockedMethodCall = Helpers.FindMockedMethodInvocationFromSetupMethod(setupGetOrSetInvocation); + InvocationExpressionSyntax? mockedMethodCall = setupGetOrSetInvocation.FindMockedMethodInvocationFromSetupMethod(); if (mockedMethodCall == null) return; ISymbol? mockedMethodSymbol = context.SemanticModel.GetSymbolInfo(mockedMethodCall, context.CancellationToken).Symbol; if (mockedMethodSymbol == null) return; - Diagnostic? diagnostic = Diagnostic.Create(Rule, mockedMethodCall.GetLocation()); + Diagnostic diagnostic = Diagnostic.Create(Rule, mockedMethodCall.GetLocation()); context.ReportDiagnostic(diagnostic); } } diff --git a/Source/Moq.Analyzers/NoSealedClassMocksAnalyzer.cs b/src/Moq.Analyzers/NoSealedClassMocksAnalyzer.cs similarity index 87% rename from Source/Moq.Analyzers/NoSealedClassMocksAnalyzer.cs rename to src/Moq.Analyzers/NoSealedClassMocksAnalyzer.cs index 58b31879..4e5d244f 100644 --- a/Source/Moq.Analyzers/NoSealedClassMocksAnalyzer.cs +++ b/src/Moq.Analyzers/NoSealedClassMocksAnalyzer.cs @@ -33,9 +33,10 @@ public override void Initialize(AnalysisContext context) context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.ObjectCreationExpression); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "AV1500:Member or local function contains too many statements", Justification = "Tracked in https://github.com/rjmurillo/moq.analyzers/issues/90")] private static void Analyze(SyntaxNodeAnalysisContext context) { - ObjectCreationExpressionSyntax? objectCreation = (ObjectCreationExpressionSyntax)context.Node; + ObjectCreationExpressionSyntax objectCreation = (ObjectCreationExpressionSyntax)context.Node; // TODO Think how to make this piece more elegant while fast GenericNameSyntax? genericName = objectCreation.Type as GenericNameSyntax; @@ -67,7 +68,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context) // Checked mocked type if (symbol.IsSealed && symbol.TypeKind != TypeKind.Delegate) { - Diagnostic? diagnostic = Diagnostic.Create(Rule, typeArguments[0].GetLocation()); + Diagnostic diagnostic = Diagnostic.Create(Rule, typeArguments[0].GetLocation()); context.ReportDiagnostic(diagnostic); } } diff --git a/Source/Moq.Analyzers/Resources.Designer.cs b/src/Moq.Analyzers/Resources.Designer.cs similarity index 100% rename from Source/Moq.Analyzers/Resources.Designer.cs rename to src/Moq.Analyzers/Resources.Designer.cs diff --git a/Source/Moq.Analyzers/Resources.resx b/src/Moq.Analyzers/Resources.resx similarity index 100% rename from Source/Moq.Analyzers/Resources.resx rename to src/Moq.Analyzers/Resources.resx diff --git a/src/Moq.Analyzers/SemanticModelExtensions.cs b/src/Moq.Analyzers/SemanticModelExtensions.cs new file mode 100644 index 00000000..f6899ae1 --- /dev/null +++ b/src/Moq.Analyzers/SemanticModelExtensions.cs @@ -0,0 +1,111 @@ +using System.Diagnostics; + +namespace Moq.Analyzers; + +/// +/// Extensions methods for . +/// +internal static class SemanticModelExtensions +{ + private static readonly MoqMethodDescriptorBase MoqSetupMethodDescriptor = new MoqSetupMethodDescriptor(); + + internal static InvocationExpressionSyntax? FindSetupMethodFromCallbackInvocation(this SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken) + { + InvocationExpressionSyntax? invocation = expression as InvocationExpressionSyntax; + if (invocation?.Expression is not MemberAccessExpressionSyntax method) return null; + if (IsMoqSetupMethod(semanticModel, method, cancellationToken)) return invocation; + return FindSetupMethodFromCallbackInvocation(semanticModel, method.Expression, cancellationToken); + } + + internal static IEnumerable GetAllMatchingMockedMethodSymbolsFromSetupMethodInvocation(this SemanticModel semanticModel, InvocationExpressionSyntax? setupMethodInvocation) + { + LambdaExpressionSyntax? setupLambdaArgument = setupMethodInvocation?.ArgumentList.Arguments[0].Expression as LambdaExpressionSyntax; + + return setupLambdaArgument?.Body is not InvocationExpressionSyntax mockedMethodInvocation + ? [] + : semanticModel.GetAllMatchingSymbols(mockedMethodInvocation); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "AV1500:Member or local function contains too many statements", Justification = "Tracked in https://github.com/rjmurillo/moq.analyzers/issues/90")] + internal static bool IsCallbackOrReturnInvocation(this SemanticModel semanticModel, InvocationExpressionSyntax callbackOrReturnsInvocation) + { + MemberAccessExpressionSyntax? callbackOrReturnsMethod = callbackOrReturnsInvocation.Expression as MemberAccessExpressionSyntax; + + if (callbackOrReturnsMethod == null) + { + return false; + } + + string methodName = callbackOrReturnsMethod.Name.ToString(); + + // First fast check before walking semantic model + if (!string.Equals(methodName, "Callback", StringComparison.Ordinal) + && !string.Equals(methodName, "Returns", StringComparison.Ordinal)) + { + return false; + } + + SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(callbackOrReturnsMethod); + return symbolInfo.CandidateReason switch + { + CandidateReason.OverloadResolutionFailure => symbolInfo.CandidateSymbols.Any(IsCallbackOrReturnSymbol), + CandidateReason.None => IsCallbackOrReturnSymbol(symbolInfo.Symbol), + _ => false, + }; + } + + internal static bool IsMoqSetupMethod(this SemanticModel semanticModel, MemberAccessExpressionSyntax method, CancellationToken cancellationToken) + { + return MoqSetupMethodDescriptor.IsMatch(semanticModel, method, cancellationToken); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "AV1500:Member or local function contains too many statements", Justification = "Tracked in https://github.com/rjmurillo/moq.analyzers/issues/90")] + private static List GetAllMatchingSymbols(this SemanticModel semanticModel, ExpressionSyntax expression) + where T : class + { + List matchingSymbols = new(); + + SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(expression); + switch (symbolInfo) + { + case { CandidateReason: CandidateReason.None, Symbol: T }: + { + T? value = symbolInfo.Symbol as T; + Debug.Assert(value != null, "Value should not be null."); + +#pragma warning disable S2589 // Boolean expressions should not be gratuitous + if (value != default(T)) + { + matchingSymbols.Add(value); + } +#pragma warning restore S2589 // Boolean expressions should not be gratuitous + break; + } + + default: + { + if (symbolInfo.CandidateReason == CandidateReason.OverloadResolutionFailure) + { + matchingSymbols.AddRange(symbolInfo.CandidateSymbols.OfType()); + } + else + { + return matchingSymbols; + } + + break; + } + } + + return matchingSymbols; + } + + private static bool IsCallbackOrReturnSymbol(ISymbol? symbol) + { + // TODO: Check what is the best way to do such checks + if (symbol is not IMethodSymbol methodSymbol) return false; + string? methodName = methodSymbol.ToString(); + return methodName.StartsWith("Moq.Language.ICallback", StringComparison.Ordinal) + || methodName.StartsWith("Moq.Language.IReturns", StringComparison.Ordinal); + } +} diff --git a/Source/Moq.Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs b/src/Moq.Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs similarity index 75% rename from Source/Moq.Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs rename to src/Moq.Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs index 4145492f..aaceb995 100644 --- a/Source/Moq.Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs +++ b/src/Moq.Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs @@ -30,13 +30,14 @@ public override void Initialize(AnalysisContext context) context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "AV1500:Member or local function contains too many statements", Justification = "Tracked in https://github.com/rjmurillo/moq.analyzers/issues/90")] private static void Analyze(SyntaxNodeAnalysisContext context) { - InvocationExpressionSyntax? setupInvocation = (InvocationExpressionSyntax)context.Node; + InvocationExpressionSyntax setupInvocation = (InvocationExpressionSyntax)context.Node; - if (setupInvocation.Expression is MemberAccessExpressionSyntax memberAccessExpression && Helpers.IsMoqSetupMethod(context.SemanticModel, memberAccessExpression, context.CancellationToken)) + if (setupInvocation.Expression is MemberAccessExpressionSyntax memberAccessExpression && context.SemanticModel.IsMoqSetupMethod(memberAccessExpression, context.CancellationToken)) { - ExpressionSyntax? mockedMemberExpression = Helpers.FindMockedMemberExpressionFromSetupMethod(setupInvocation); + ExpressionSyntax? mockedMemberExpression = setupInvocation.FindMockedMemberExpressionFromSetupMethod(); if (mockedMemberExpression == null) { return; @@ -46,7 +47,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context) if (symbolInfo.Symbol is IPropertySymbol or IMethodSymbol && !IsMethodOverridable(symbolInfo.Symbol)) { - Diagnostic? diagnostic = Diagnostic.Create(Rule, mockedMemberExpression.GetLocation()); + Diagnostic diagnostic = Diagnostic.Create(Rule, mockedMemberExpression.GetLocation()); context.ReportDiagnostic(diagnostic); } } diff --git a/Source/Moq.Analyzers/SetupShouldNotIncludeAsyncResultAnalyzer.cs b/src/Moq.Analyzers/SetupShouldNotIncludeAsyncResultAnalyzer.cs similarity index 78% rename from Source/Moq.Analyzers/SetupShouldNotIncludeAsyncResultAnalyzer.cs rename to src/Moq.Analyzers/SetupShouldNotIncludeAsyncResultAnalyzer.cs index db703d7a..44f861b3 100644 --- a/Source/Moq.Analyzers/SetupShouldNotIncludeAsyncResultAnalyzer.cs +++ b/src/Moq.Analyzers/SetupShouldNotIncludeAsyncResultAnalyzer.cs @@ -30,13 +30,14 @@ public override void Initialize(AnalysisContext context) context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "AV1500:Member or local function contains too many statements", Justification = "Tracked in https://github.com/rjmurillo/moq.analyzers/issues/90")] private static void Analyze(SyntaxNodeAnalysisContext context) { - InvocationExpressionSyntax? setupInvocation = (InvocationExpressionSyntax)context.Node; + InvocationExpressionSyntax setupInvocation = (InvocationExpressionSyntax)context.Node; - if (setupInvocation.Expression is MemberAccessExpressionSyntax memberAccessExpression && Helpers.IsMoqSetupMethod(context.SemanticModel, memberAccessExpression, context.CancellationToken)) + if (setupInvocation.Expression is MemberAccessExpressionSyntax memberAccessExpression && context.SemanticModel.IsMoqSetupMethod(memberAccessExpression, context.CancellationToken)) { - ExpressionSyntax? mockedMemberExpression = Helpers.FindMockedMemberExpressionFromSetupMethod(setupInvocation); + ExpressionSyntax? mockedMemberExpression = setupInvocation.FindMockedMemberExpressionFromSetupMethod(); if (mockedMemberExpression == null) { return; @@ -47,7 +48,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context) && !IsMethodOverridable(symbolInfo.Symbol) && IsMethodReturnTypeTask(symbolInfo.Symbol)) { - Diagnostic? diagnostic = Diagnostic.Create(Rule, mockedMemberExpression.GetLocation()); + Diagnostic diagnostic = Diagnostic.Create(Rule, mockedMemberExpression.GetLocation()); context.ReportDiagnostic(diagnostic); } } @@ -61,7 +62,7 @@ private static bool IsMethodOverridable(ISymbol methodSymbol) private static bool IsMethodReturnTypeTask(ISymbol methodSymbol) { - string? type = methodSymbol.ToDisplayString(); + string type = methodSymbol.ToDisplayString(); return string.Equals(type, "System.Threading.Tasks.Task", StringComparison.Ordinal) || string.Equals(type, "System.Threading.ValueTask", StringComparison.Ordinal) || type.StartsWith("System.Threading.Tasks.Task<", StringComparison.Ordinal) diff --git a/Source/Moq.Analyzers/tools/install.ps1 b/src/Moq.Analyzers/tools/install.ps1 similarity index 100% rename from Source/Moq.Analyzers/tools/install.ps1 rename to src/Moq.Analyzers/tools/install.ps1 diff --git a/Source/Moq.Analyzers/tools/uninstall.ps1 b/src/Moq.Analyzers/tools/uninstall.ps1 similarity index 100% rename from Source/Moq.Analyzers/tools/uninstall.ps1 rename to src/Moq.Analyzers/tools/uninstall.ps1 diff --git a/Source/Moq.Analyzers.Test/.editorconfig b/tests/.editorconfig similarity index 64% rename from Source/Moq.Analyzers.Test/.editorconfig rename to tests/.editorconfig index 7f840f54..576433c2 100644 --- a/Source/Moq.Analyzers.Test/.editorconfig +++ b/tests/.editorconfig @@ -12,3 +12,9 @@ dotnet_diagnostic.CS1591.severity = suggestion # CS1712: Type parameter 'type parameter' has no matching typeparam tag in the XML comment on 'type' (but other type parameters do) dotnet_diagnostic.CS1712.severity = suggestion + +# VSTHRD200: Use "Async" suffix for async methods +# AV1755: Postfix asynchronous methods with Async or TaskAsync +# Just about every test method is async, doesn't provide any real value and clustters up test window +dotnet_diagnostic.VSTHRD200.severity = none +dotnet_diagnostic.AV1755.severity = none diff --git a/tests/Moq.Analyzers.Benchmarks/Constants.cs b/tests/Moq.Analyzers.Benchmarks/Constants.cs new file mode 100644 index 00000000..338f9f04 --- /dev/null +++ b/tests/Moq.Analyzers.Benchmarks/Constants.cs @@ -0,0 +1,6 @@ +namespace Moq.Analyzers.Benchmarks; + +internal static class Constants +{ + public const int NumberOfCodeFiles = 1_000; +} diff --git a/tests/Moq.Analyzers.Benchmarks/Helpers/AnalysisResultExtensions.cs b/tests/Moq.Analyzers.Benchmarks/Helpers/AnalysisResultExtensions.cs new file mode 100644 index 00000000..c9dc8ade --- /dev/null +++ b/tests/Moq.Analyzers.Benchmarks/Helpers/AnalysisResultExtensions.cs @@ -0,0 +1,21 @@ +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Moq.Analyzers.Benchmarks.Helpers; + +internal static class AnalysisResultExtensions +{ + public static AnalysisResult AssertValidAnalysisResult(this AnalysisResult analysisResult) + { + if (analysisResult.Analyzers.Length != 1) + { + throw new InvalidOperationException($"Expected a single analyzer but found '{analysisResult.Analyzers.Length}'"); + } + + if (analysisResult.CompilationDiagnostics.Count != 0) + { + throw new InvalidOperationException($"Expected no compilation diagnostics but found '{analysisResult.CompilationDiagnostics.Count}'"); + } + + return analysisResult; + } +} diff --git a/tests/Moq.Analyzers.Benchmarks/Helpers/AsyncLazy.cs b/tests/Moq.Analyzers.Benchmarks/Helpers/AsyncLazy.cs new file mode 100644 index 00000000..143dabc1 --- /dev/null +++ b/tests/Moq.Analyzers.Benchmarks/Helpers/AsyncLazy.cs @@ -0,0 +1,46 @@ +using System.Runtime.CompilerServices; + +namespace Moq.Analyzers.Benchmarks.Helpers; + +internal class AsyncLazy : Lazy> +{ + public AsyncLazy( + Func valueFactory, + CancellationToken cancellationToken, + TaskScheduler? scheduler = null, + TaskCreationOptions taskCreationOptions = TaskCreationOptions.None, + LazyThreadSafetyMode mode = LazyThreadSafetyMode.ExecutionAndPublication) + : base( + () => + Task.Factory.StartNew( + valueFactory, + cancellationToken, + taskCreationOptions, + scheduler ?? TaskScheduler.Default), + mode) + { + } + + public AsyncLazy( + Func> taskFactory, + CancellationToken cancellationToken, + TaskScheduler? scheduler = null, + TaskCreationOptions taskCreationOptions = TaskCreationOptions.None, + LazyThreadSafetyMode mode = LazyThreadSafetyMode.ExecutionAndPublication) + : base( + () => + Task.Factory.StartNew( + () => taskFactory(), + cancellationToken, + taskCreationOptions, + scheduler ?? TaskScheduler.Default) + .Unwrap(), + mode) + { + } + + public TaskAwaiter GetAwaiter() + { + return Value.GetAwaiter(); + } +} diff --git a/tests/Moq.Analyzers.Benchmarks/Helpers/BenchmarkCSharpCompilationCreator.cs b/tests/Moq.Analyzers.Benchmarks/Helpers/BenchmarkCSharpCompilationCreator.cs new file mode 100644 index 00000000..7f90cb41 --- /dev/null +++ b/tests/Moq.Analyzers.Benchmarks/Helpers/BenchmarkCSharpCompilationCreator.cs @@ -0,0 +1,26 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; + +namespace Moq.Analyzers.Benchmarks.Helpers; + +internal static class BenchmarkCSharpCompilationCreator + where TAnalyzer : DiagnosticAnalyzer, new() +{ + public static async Task<(CompilationWithAnalyzers Baseline, CompilationWithAnalyzers Test)> CreateAsync( + (string Name, string Contents)[] sources, + AnalyzerOptions? options = null) + { + Compilation? compilation = await CSharpCompilationCreator.CreateAsync(sources).ConfigureAwait(false); + + if (compilation is null) + { + throw new InvalidOperationException("Failed to create compilation"); + } + + CompilationWithAnalyzers baseline = compilation.WithAnalyzers([new EmptyDiagnosticAnalyzer()], options, CancellationToken.None); + CompilationWithAnalyzers test = compilation.WithAnalyzers([new TAnalyzer()], options, CancellationToken.None); + + return (baseline, test); + } +} diff --git a/tests/Moq.Analyzers.Benchmarks/Helpers/CSharpCompilationCreator.cs b/tests/Moq.Analyzers.Benchmarks/Helpers/CSharpCompilationCreator.cs new file mode 100644 index 00000000..c829e9b2 --- /dev/null +++ b/tests/Moq.Analyzers.Benchmarks/Helpers/CSharpCompilationCreator.cs @@ -0,0 +1,35 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Moq.Analyzers.Benchmarks.Helpers; + +namespace Moq.Analyzers.Benchmarks; + +// Originally from https://github.com/dotnet/roslyn-analyzers/blob/f1115edce8633ebe03a86191bc05c6969ed9a821/src/PerformanceTests/Utilities/CSharp/CSharpCompilationHelper.cs +// See https://github.com/dotnet/roslyn-sdk/issues/1165 for discussion on providing these or similar helpers in the testing packages. +internal static class CSharpCompilationCreator +{ + public static async Task CreateAsync((string, string)[] sourceFiles) + { + (Project project, _) = await CreateProjectAsync(sourceFiles, globalOptions: null).ConfigureAwait(false); + return await project.GetCompilationAsync().ConfigureAwait(false); + } + + public static async Task<(Compilation? Compilation, AnalyzerOptions Options)> CreateWithOptionsAsync((string, string)[] sourceFiles, (string, string)[] globalOptions) + { + (Project project, AnalyzerOptions options) = await CreateProjectAsync(sourceFiles, globalOptions).ConfigureAwait(false); + return (await project.GetCompilationAsync().ConfigureAwait(false), options); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "AV1553:Do not use optional parameters with default value null for strings, collections or tasks", Justification = "Minimizing divergence from upstream code")] + private static Task<(Project Project, AnalyzerOptions Options)> CreateProjectAsync((string, string)[] sourceFiles, (string, string)[]? globalOptions = null) + => CompilationCreator.CreateProjectAsync( + sourceFiles, + globalOptions, + "TestProject", + LanguageNames.CSharp, + "/0/Test", + "cs", + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary), + new CSharpParseOptions(LanguageVersion.Default)); +} diff --git a/tests/Moq.Analyzers.Benchmarks/Helpers/CompilationCreator.cs b/tests/Moq.Analyzers.Benchmarks/Helpers/CompilationCreator.cs new file mode 100644 index 00000000..d3c73a03 --- /dev/null +++ b/tests/Moq.Analyzers.Benchmarks/Helpers/CompilationCreator.cs @@ -0,0 +1,159 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Testing.Model; +using Microsoft.VisualStudio.Composition; + +namespace Moq.Analyzers.Benchmarks.Helpers; + +// Originally from https://github.com/dotnet/roslyn-analyzers/blob/f1115edce8633ebe03a86191bc05c6969ed9a821/src/PerformanceTests/Utilities/Common/CompilationHelper.cs +// See https://github.com/dotnet/roslyn-sdk/issues/1165 for discussion on providing these or similar helpers in the testing packages. +internal static class CompilationCreator +{ + private static readonly ReferenceAssemblies ReferenceAssemblies = ReferenceAssemblies.Net.Net80.AddPackages([new PackageIdentity("Moq", "4.18.4")]); + + [SuppressMessage("Maintainability", "AV1500:Member or local function contains too many statements", Justification = "Minimizing divergence from upstream code.")] + public static async Task<(Project Project, AnalyzerOptions Options)> CreateProjectAsync( + (string, string)[] sourceFiles, + (string, string)[]? globalOptions, + string name, + string language, + string defaultPrefix, + string defaultExtension, + CompilationOptions compilationOptions, + ParseOptions parseOptions) + { + ProjectState projectState = new ProjectState(name, language, defaultPrefix, defaultExtension); + foreach ((string filename, string content) in sourceFiles) + { + projectState.Sources.Add((defaultPrefix + filename + "." + defaultExtension, content)); + } + + EvaluatedProjectState evaluatedProj = new EvaluatedProjectState(projectState, ReferenceAssemblies); + + Project project = await CreateProjectAsync(evaluatedProj, compilationOptions, parseOptions).ConfigureAwait(false); + + if (globalOptions is not null) + { + OptionsProvider optionsProvider = new(globalOptions); + AnalyzerOptions options = new(ImmutableArray.Empty, optionsProvider); + + return (project, options); + } + + return (project, project.AnalyzerOptions); + } + + [SuppressMessage("Maintainability", "AV1500:Member or local function contains too many statements", Justification = "Minimizing divergence with upstream code")] + [SuppressMessage("Maintainability", "AV1551:Method overload should call another overload", Justification = "Minimizing divergence with upstream code")] + [SuppressMessage("Maintainability", "AV1555:Avoid using non-(nullable-)boolean named arguments", Justification = "Minimizing divergence with upstream code")] + private static async Task CreateProjectAsync( + EvaluatedProjectState primaryProject, + CompilationOptions compilationOptions, + ParseOptions parseOptions) + { + ProjectId projectId = ProjectId.CreateNewId(debugName: primaryProject.Name); + Solution solution = await CreateSolutionAsync(projectId, primaryProject, compilationOptions, parseOptions).ConfigureAwait(false); + + foreach ((string newFileName, Microsoft.CodeAnalysis.Text.SourceText source) in primaryProject.Sources) + { + DocumentId documentId = DocumentId.CreateNewId(projectId, debugName: newFileName); + solution = solution.AddDocument(documentId, newFileName, source, filePath: newFileName); + } + + foreach ((string newFileName, Microsoft.CodeAnalysis.Text.SourceText source) in primaryProject.AdditionalFiles) + { + DocumentId documentId = DocumentId.CreateNewId(projectId, debugName: newFileName); + solution = solution.AddAdditionalDocument(documentId, newFileName, source, filePath: newFileName); + } + + foreach ((string newFileName, Microsoft.CodeAnalysis.Text.SourceText source) in primaryProject.AnalyzerConfigFiles) + { + DocumentId documentId = DocumentId.CreateNewId(projectId, debugName: newFileName); + solution = solution.AddAnalyzerConfigDocument(documentId, newFileName, source, filePath: newFileName); + } + + return solution.GetProject(projectId)!; + } + + [SuppressMessage("Maintainability", "AV1500:Member or local function contains too many statements", Justification = "Minimizing divergence from upstream")] + [SuppressMessage("Maintainability", "AV1561:Signature contains too many parameters", Justification = "Minimizing divergence from upstream")] + private static async Task CreateSolutionAsync( + ProjectId projectId, + EvaluatedProjectState projectState, + CompilationOptions compilationOptions, + ParseOptions parseOptions) + { + ReferenceAssemblies referenceAssemblies = projectState.ReferenceAssemblies ?? ReferenceAssemblies.Default; + + compilationOptions = compilationOptions + .WithOutputKind(projectState.OutputKind) + .WithAssemblyIdentityComparer(referenceAssemblies.AssemblyIdentityComparer); + + parseOptions = parseOptions + .WithDocumentationMode(projectState.DocumentationMode); + + AsyncLazy exportProviderFactory = new( + async () => + { + AttributedPartDiscovery discovery = new(Resolver.DefaultInstance, isNonPublicSupported: true); + DiscoveredParts parts = await discovery.CreatePartsAsync(MefHostServices.DefaultAssemblies).ConfigureAwait(false); + ComposableCatalog catalog = ComposableCatalog.Create(Resolver.DefaultInstance).AddParts(parts); + + CompositionConfiguration configuration = CompositionConfiguration.Create(catalog); + RuntimeComposition runtimeComposition = RuntimeComposition.CreateRuntimeComposition(configuration); + return runtimeComposition.CreateExportProviderFactory(); + }, + CancellationToken.None); + ExportProvider exportProvider = (await exportProviderFactory).CreateExportProvider(); + MefHostServices host = MefHostServices.Create(exportProvider.AsCompositionContext()); + AdhocWorkspace workspace = new AdhocWorkspace(host); + + Solution solution = workspace + .CurrentSolution + .AddProject(projectId, projectState.Name, projectState.Name, projectState.Language) + .WithProjectCompilationOptions(projectId, compilationOptions) + .WithProjectParseOptions(projectId, parseOptions); + + ImmutableArray metadataReferences = await referenceAssemblies.ResolveAsync(projectState.Language, CancellationToken.None).ConfigureAwait(false); + solution = solution.AddMetadataReferences(projectId, metadataReferences); + + return solution; + } + + /// + /// This class just passes argument through to the projects options provider and it used to provider custom global options. + /// + private sealed class OptionsProvider : AnalyzerConfigOptionsProvider + { + public OptionsProvider((string, string)[] globalOptions) + { + GlobalOptions = new ConfigOptions(globalOptions); + } + + public override AnalyzerConfigOptions GlobalOptions { get; } + + public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) + => GlobalOptions; + + public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) + => GlobalOptions; + } + + /// + /// Allows adding additional global options. + /// + private sealed class ConfigOptions : AnalyzerConfigOptions + { + private readonly Dictionary _globalOptions; + + public ConfigOptions((string, string)[] globalOptions) + => _globalOptions = globalOptions.ToDictionary(kvp => kvp.Item1, kvp => kvp.Item2, StringComparer.OrdinalIgnoreCase); + + public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value) + => _globalOptions.TryGetValue(key, out value); + } +} diff --git a/tests/Moq.Analyzers.Benchmarks/Helpers/ExportProviderExtensions.cs b/tests/Moq.Analyzers.Benchmarks/Helpers/ExportProviderExtensions.cs new file mode 100644 index 00000000..57323710 --- /dev/null +++ b/tests/Moq.Analyzers.Benchmarks/Helpers/ExportProviderExtensions.cs @@ -0,0 +1,89 @@ +using System.Composition; +using System.Composition.Hosting.Core; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Microsoft.VisualStudio.Composition; + +namespace Moq.Analyzers.Benchmarks.Helpers; + +// Originally from https://github.com/dotnet/roslyn-analyzers/blob/f1115edce8633ebe03a86191bc05c6969ed9a821/src/PerformanceTests/Utilities/Common/ExportProviderExtensions.cs +// See https://github.com/dotnet/roslyn-sdk/issues/1165 for discussion on providing these or similar helpers in the testing packages. +internal static class ExportProviderExtensions +{ + public static CompositionContext AsCompositionContext(this ExportProvider exportProvider) + { + return new CompositionContextShim(exportProvider); + } + + private sealed class CompositionContextShim : CompositionContext + { + private readonly ExportProvider _exportProvider; + + public CompositionContextShim(ExportProvider exportProvider) + { + _exportProvider = exportProvider; + } + + [SuppressMessage("Maintainability", "AV1500:Member or local function contains too many statements", Justification = "Minimizing divergence from upstream")] + public override bool TryGetExport(CompositionContract contract, [NotNullWhen(true)] out object? export) + { + bool importMany = contract.MetadataConstraints.Contains(new KeyValuePair("IsImportMany", true)); + (Type contractType, Type? metadataType) = GetContractType(contract.ContractType, importMany); + + if (metadataType != null) + { + MethodInfo methodInfo = (from method in _exportProvider.GetType().GetTypeInfo().GetMethods() + where string.Equals(method.Name, nameof(ExportProvider.GetExports), StringComparison.Ordinal) + where method.IsGenericMethod && method.GetGenericArguments().Length == 2 + where method.GetParameters().Length == 1 && method.GetParameters()[0].ParameterType == typeof(string) + select method).Single(); + MethodInfo parameterizedMethod = methodInfo.MakeGenericMethod(contractType, metadataType); + export = parameterizedMethod.Invoke(_exportProvider, [contract.ContractName]); + } + else + { + MethodInfo methodInfo = (from method in _exportProvider.GetType().GetTypeInfo().GetMethods() + where string.Equals(method.Name, nameof(ExportProvider.GetExports), StringComparison.Ordinal) + where method.IsGenericMethod && method.GetGenericArguments().Length == 1 + where method.GetParameters().Length == 1 && method.GetParameters()[0].ParameterType == typeof(string) + select method).Single(); + MethodInfo parameterizedMethod = methodInfo.MakeGenericMethod(contractType); + export = parameterizedMethod.Invoke(_exportProvider, [contract.ContractName]); + } + +#pragma warning disable CS8762 // Parameter must have a non-null value when exiting in some condition. + return true; +#pragma warning restore CS8762 // Parameter must have a non-null value when exiting in some condition. + } + + [SuppressMessage("Maintainability", "AV1500:Member or local function contains too many statements", Justification = "Minimizing divergence from upstream")] + private static (Type ExportType, Type? MetadataType) GetContractType(Type contractType, bool importMany) + { + if (importMany && contractType.IsConstructedGenericType && + (contractType.GetGenericTypeDefinition() == typeof(IList<>) + || contractType.GetGenericTypeDefinition() == typeof(ICollection<>) + || contractType.GetGenericTypeDefinition() == typeof(IEnumerable<>))) + { + contractType = contractType.GenericTypeArguments[0]; + } + + if (contractType.IsConstructedGenericType) + { + if (contractType.GetGenericTypeDefinition() == typeof(Lazy<>)) + { + return (contractType.GenericTypeArguments[0], null); + } + else if (contractType.GetGenericTypeDefinition() == typeof(Lazy<,>)) + { + return (contractType.GenericTypeArguments[0], contractType.GenericTypeArguments[1]); + } + else + { + throw new NotSupportedException(); + } + } + + throw new NotSupportedException(); + } + } +} diff --git a/tests/Moq.Analyzers.Benchmarks/Moq.Analyzers.Benchmarks.csproj b/tests/Moq.Analyzers.Benchmarks/Moq.Analyzers.Benchmarks.csproj new file mode 100644 index 00000000..99cc5890 --- /dev/null +++ b/tests/Moq.Analyzers.Benchmarks/Moq.Analyzers.Benchmarks.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + false + + + + + + + + + + diff --git a/tests/Moq.Analyzers.Benchmarks/Moq1300Benchmarks.cs b/tests/Moq.Analyzers.Benchmarks/Moq1300Benchmarks.cs new file mode 100644 index 00000000..c8006602 --- /dev/null +++ b/tests/Moq.Analyzers.Benchmarks/Moq1300Benchmarks.cs @@ -0,0 +1,84 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using BenchmarkDotNet.Attributes; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Moq.Analyzers.Benchmarks.Helpers; + +namespace Moq.Analyzers.Benchmarks; + +[InProcess] +[MemoryDiagnoser] +public class Moq1300Benchmarks +{ + private static CompilationWithAnalyzers? BaselineCompilation { get; set; } + + private static CompilationWithAnalyzers? TestCompilation { get; set; } + + [IterationSetup] + [SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Async setup not supported in BenchmarkDotNet.See https://github.com/dotnet/BenchmarkDotNet/issues/2442.")] + public static void SetupCompilation() + { + List<(string Name, string Content)> sources = []; + for (int index = 0; index < Constants.NumberOfCodeFiles; index++) + { + string name = "TypeName" + index; + sources.Add((name, @$" +using System; +using Moq; + +public class SampleClass{index} +{{ + + public int Calculate() => 0; +}} + +internal class {name} +{{ + private void Test() + {{ + new Mock().As(); + }} +}} +")); + } + + (BaselineCompilation, TestCompilation) = + BenchmarkCSharpCompilationCreator + .CreateAsync(sources.ToArray()) + .GetAwaiter() + .GetResult(); + } + + [Benchmark] + public async Task Moq1300WithDiagnostics() + { + ImmutableArray diagnostics = + (await TestCompilation! + .GetAnalysisResultAsync(CancellationToken.None) + .ConfigureAwait(false)) + .AssertValidAnalysisResult() + .GetAllDiagnostics(); + + if (diagnostics.Length != Constants.NumberOfCodeFiles) + { + throw new InvalidOperationException($"Expected '{Constants.NumberOfCodeFiles:N0}' analyzer diagnostics but found '{diagnostics.Length}'"); + } + } + + [Benchmark(Baseline = true)] + public async Task Moq1300Baseline() + { + ImmutableArray diagnostics = + (await BaselineCompilation! + .GetAnalysisResultAsync(CancellationToken.None) + .ConfigureAwait(false)) + .AssertValidAnalysisResult() + .GetAllDiagnostics(); + + if (diagnostics.Length != 0) + { + throw new InvalidOperationException($"Expected no analyzer diagnostics but found '{diagnostics.Length}'"); + } + } +} diff --git a/tests/Moq.Analyzers.Benchmarks/Program.cs b/tests/Moq.Analyzers.Benchmarks/Program.cs new file mode 100644 index 00000000..fbd12647 --- /dev/null +++ b/tests/Moq.Analyzers.Benchmarks/Program.cs @@ -0,0 +1,19 @@ +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Running; + +namespace Moq.Analyzers.Benchmarks; + +/// +/// Entrypoint for benchmarks. +/// +public static class Program +{ + /// + /// Main entrypoint for benchmarks. + /// + /// Command line arguments. + public static void Main(string[] args) + => BenchmarkSwitcher + .FromAssembly(typeof(Program).Assembly) + .Run(args, DefaultConfig.Instance.WithOptions(ConfigOptions.DisableOptimizationsValidator)); // Needed because Microsoft.CodeAnalysis.Testing does not build with optimizations. See https://github.com/dotnet/roslyn-sdk/issues/1165. +} diff --git a/Source/Moq.Analyzers.Test/AsAcceptOnlyInterfaceAnalyzerTests.cs b/tests/Moq.Analyzers.Test/AsAcceptOnlyInterfaceAnalyzerTests.cs similarity index 100% rename from Source/Moq.Analyzers.Test/AsAcceptOnlyInterfaceAnalyzerTests.cs rename to tests/Moq.Analyzers.Test/AsAcceptOnlyInterfaceAnalyzerTests.cs diff --git a/Source/Moq.Analyzers.Test/CallbackSignatureShouldMatchMockedMethodCodeFixTests.cs b/tests/Moq.Analyzers.Test/CallbackSignatureShouldMatchMockedMethodCodeFixTests.cs similarity index 97% rename from Source/Moq.Analyzers.Test/CallbackSignatureShouldMatchMockedMethodCodeFixTests.cs rename to tests/Moq.Analyzers.Test/CallbackSignatureShouldMatchMockedMethodCodeFixTests.cs index af3280c0..23df15cc 100644 --- a/Source/Moq.Analyzers.Test/CallbackSignatureShouldMatchMockedMethodCodeFixTests.cs +++ b/tests/Moq.Analyzers.Test/CallbackSignatureShouldMatchMockedMethodCodeFixTests.cs @@ -4,6 +4,7 @@ namespace Moq.Analyzers.Test; public class CallbackSignatureShouldMatchMockedMethodCodeFixTests { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "MA0051:Method is too long", Justification = "Contains test data")] public static IEnumerable TestData() { return new object[][] diff --git a/Source/Moq.Analyzers.Test/ConstructorArgumentsShouldMatchAnalyzerTests.cs b/tests/Moq.Analyzers.Test/ConstructorArgumentsShouldMatchAnalyzerTests.cs similarity index 99% rename from Source/Moq.Analyzers.Test/ConstructorArgumentsShouldMatchAnalyzerTests.cs rename to tests/Moq.Analyzers.Test/ConstructorArgumentsShouldMatchAnalyzerTests.cs index f7dcf990..4ce25e00 100644 --- a/Source/Moq.Analyzers.Test/ConstructorArgumentsShouldMatchAnalyzerTests.cs +++ b/tests/Moq.Analyzers.Test/ConstructorArgumentsShouldMatchAnalyzerTests.cs @@ -35,6 +35,7 @@ public static IEnumerable TestData() ["""new Mock>{|Moq1002:(42)|};"""], ["""new Mock>();"""], ["""new Mock>(MockBehavior.Default);"""], + // TODO: "I think this _should_ fail, but currently passes. Tracked by #55." // ["""new Mock();"""], ["""new Mock{|Moq1002:("42")|};"""], diff --git a/Source/Moq.Analyzers/GlobalSuppressions.cs b/tests/Moq.Analyzers.Test/GlobalSuppressions.cs similarity index 100% rename from Source/Moq.Analyzers/GlobalSuppressions.cs rename to tests/Moq.Analyzers.Test/GlobalSuppressions.cs diff --git a/Source/Moq.Analyzers.Test/GlobalUsings.cs b/tests/Moq.Analyzers.Test/GlobalUsings.cs similarity index 100% rename from Source/Moq.Analyzers.Test/GlobalUsings.cs rename to tests/Moq.Analyzers.Test/GlobalUsings.cs diff --git a/Source/Moq.Analyzers.Test/Helpers/AnalyzerVerifier.cs b/tests/Moq.Analyzers.Test/Helpers/AnalyzerVerifier.cs similarity index 100% rename from Source/Moq.Analyzers.Test/Helpers/AnalyzerVerifier.cs rename to tests/Moq.Analyzers.Test/Helpers/AnalyzerVerifier.cs diff --git a/Source/Moq.Analyzers.Test/Helpers/CodeFixVerifier.cs b/tests/Moq.Analyzers.Test/Helpers/CodeFixVerifier.cs similarity index 100% rename from Source/Moq.Analyzers.Test/Helpers/CodeFixVerifier.cs rename to tests/Moq.Analyzers.Test/Helpers/CodeFixVerifier.cs diff --git a/Source/Moq.Analyzers.Test/Helpers/ReferenceAssemblyCatalog.cs b/tests/Moq.Analyzers.Test/Helpers/ReferenceAssemblyCatalog.cs similarity index 100% rename from Source/Moq.Analyzers.Test/Helpers/ReferenceAssemblyCatalog.cs rename to tests/Moq.Analyzers.Test/Helpers/ReferenceAssemblyCatalog.cs diff --git a/Source/Moq.Analyzers.Test/Helpers/Test.cs b/tests/Moq.Analyzers.Test/Helpers/Test.cs similarity index 100% rename from Source/Moq.Analyzers.Test/Helpers/Test.cs rename to tests/Moq.Analyzers.Test/Helpers/Test.cs diff --git a/Source/Moq.Analyzers.Test/Helpers/TestDataExtensions.cs b/tests/Moq.Analyzers.Test/Helpers/TestDataExtensions.cs similarity index 100% rename from Source/Moq.Analyzers.Test/Helpers/TestDataExtensions.cs rename to tests/Moq.Analyzers.Test/Helpers/TestDataExtensions.cs diff --git a/Source/Moq.Analyzers.Test/ModuleInitializer.cs b/tests/Moq.Analyzers.Test/ModuleInitializer.cs similarity index 100% rename from Source/Moq.Analyzers.Test/ModuleInitializer.cs rename to tests/Moq.Analyzers.Test/ModuleInitializer.cs diff --git a/Source/Moq.Analyzers.Test/Moq.Analyzers.Test.csproj b/tests/Moq.Analyzers.Test/Moq.Analyzers.Test.csproj similarity index 76% rename from Source/Moq.Analyzers.Test/Moq.Analyzers.Test.csproj rename to tests/Moq.Analyzers.Test/Moq.Analyzers.Test.csproj index a5f5f708..43af6d3e 100644 --- a/Source/Moq.Analyzers.Test/Moq.Analyzers.Test.csproj +++ b/tests/Moq.Analyzers.Test/Moq.Analyzers.Test.csproj @@ -7,7 +7,6 @@ - 1701;1702;SA1600;SA1402 @@ -19,14 +18,12 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + \ No newline at end of file diff --git a/Source/Moq.Analyzers.Test/NoConstructorArgumentsForInterfaceMockAnalyzerTests.cs b/tests/Moq.Analyzers.Test/NoConstructorArgumentsForInterfaceMockAnalyzerTests.cs similarity index 100% rename from Source/Moq.Analyzers.Test/NoConstructorArgumentsForInterfaceMockAnalyzerTests.cs rename to tests/Moq.Analyzers.Test/NoConstructorArgumentsForInterfaceMockAnalyzerTests.cs diff --git a/Source/Moq.Analyzers.Test/NoMethodsInPropertySetupAnalyzerTests.cs b/tests/Moq.Analyzers.Test/NoMethodsInPropertySetupAnalyzerTests.cs similarity index 100% rename from Source/Moq.Analyzers.Test/NoMethodsInPropertySetupAnalyzerTests.cs rename to tests/Moq.Analyzers.Test/NoMethodsInPropertySetupAnalyzerTests.cs diff --git a/Source/Moq.Analyzers.Test/NoSealedClassMocksAnalyzerTests.cs b/tests/Moq.Analyzers.Test/NoSealedClassMocksAnalyzerTests.cs similarity index 100% rename from Source/Moq.Analyzers.Test/NoSealedClassMocksAnalyzerTests.cs rename to tests/Moq.Analyzers.Test/NoSealedClassMocksAnalyzerTests.cs diff --git a/Source/Moq.Analyzers.Test/PackageTests.Baseline#contents.verified.txt b/tests/Moq.Analyzers.Test/PackageTests.Baseline#contents.verified.txt similarity index 100% rename from Source/Moq.Analyzers.Test/PackageTests.Baseline#contents.verified.txt rename to tests/Moq.Analyzers.Test/PackageTests.Baseline#contents.verified.txt diff --git a/Source/Moq.Analyzers.Test/PackageTests.Baseline#manifest.verified.nuspec b/tests/Moq.Analyzers.Test/PackageTests.Baseline#manifest.verified.nuspec similarity index 100% rename from Source/Moq.Analyzers.Test/PackageTests.Baseline#manifest.verified.nuspec rename to tests/Moq.Analyzers.Test/PackageTests.Baseline#manifest.verified.nuspec diff --git a/tests/Moq.Analyzers.Test/PackageTests.cs b/tests/Moq.Analyzers.Test/PackageTests.cs new file mode 100644 index 00000000..8eae37bb --- /dev/null +++ b/tests/Moq.Analyzers.Test/PackageTests.cs @@ -0,0 +1,18 @@ +using System.Reflection; + +namespace Moq.Analyzers.Test; + +public class PackageTests +{ + private static readonly FileInfo Package = new FileInfo(Assembly.GetExecutingAssembly().Location) + .Directory! + .GetFiles("Moq.Analyzers*.nupkg") + .OrderByDescending(fileInfo => fileInfo.LastWriteTimeUtc) + .First(); + + [Fact] + public Task Baseline() + { + return VerifyFile(Package).ScrubNuspec(); + } +} diff --git a/Source/Moq.Analyzers.Test/SetupShouldBeUsedOnlyForOverridableMembersAnalyzerTests.cs b/tests/Moq.Analyzers.Test/SetupShouldBeUsedOnlyForOverridableMembersAnalyzerTests.cs similarity index 100% rename from Source/Moq.Analyzers.Test/SetupShouldBeUsedOnlyForOverridableMembersAnalyzerTests.cs rename to tests/Moq.Analyzers.Test/SetupShouldBeUsedOnlyForOverridableMembersAnalyzerTests.cs diff --git a/Source/Moq.Analyzers.Test/SetupShouldNotIncludeAsyncResultAnalyzerTests.cs b/tests/Moq.Analyzers.Test/SetupShouldNotIncludeAsyncResultAnalyzerTests.cs similarity index 100% rename from Source/Moq.Analyzers.Test/SetupShouldNotIncludeAsyncResultAnalyzerTests.cs rename to tests/Moq.Analyzers.Test/SetupShouldNotIncludeAsyncResultAnalyzerTests.cs