diff --git a/.agents/skills/bundle-dependency-prs/SKILL.md b/.agents/skills/bundle-dependency-prs/SKILL.md new file mode 100644 index 000000000..3fec66838 --- /dev/null +++ b/.agents/skills/bundle-dependency-prs/SKILL.md @@ -0,0 +1,46 @@ +--- +name: bundle-dependency-prs +description: Fix broken dependency update PRs and aggregate the ones that work into one PR. +disable-model-invocation: true +--- + +# Instructions + +You have two goals: + +1. Get all dependency PRs to a state where their PR checks pass. +2. Aggregate dependency PRs with passing checks into just one PR. + +You can identify dependency update PRs by those authored by `dependabot` or `renovate`. + +You'll find instructions for building and validating the repo in the [CONTRIBUTING.md](../../../CONTRIBUTING.md) doc. +Always validate your changes locally before pushing them to the remote repository. + +When writing PR bodies or comments, avoid unmatched markdown code fences. Keep markdown well-formed. + +For purposes of assessing PR readiness by its PR checks, consider docfx related checks to be irrelevant. +If a docfx check fails but all other checks succeed, then that is a 'successful' dependency update PR. + +## Fix up dependency PRs with failing checks + +Before aggregating PRs, first try to fix any individual dependency update PRs with failing build/test checks. + +1. For the dependency PRs with failing build or test PR checks, check out their source branch and fix any issues. +2. Push your fixes as fresh commits to the individual dependency PRs. + If pushing to a repo in the `microsoft` org when the PR is authored by `renovate`, follow-up the push with a PR comment that says exactly this: "/azp run" which triggers PR checks to re-run. +3. If you can't fix a particular PR, add a comment to the PR describing your attempt and outcome. + +## Group dependency PRs that are ready to go + +Your next goal is to collect all the dependency updates that are ready to go into a single PR. + +1. Prepare a local branch called `bulkDepUpdates`. + 1. Consider that a remote branch by the same name may already exist. If it does, base your local branch on it. + 2. Merge `origin/main` into this branch. + 3. Resolve any conflicts. +2. For the dependency PRs whose build and test PR checks already pass, merge them into the `bulkDepUpdates` branch. + Consider that your local branch may have already merged an equivalent PR in the past (from a past run). If so, you should skip merging that PR. + Resolve any conflicts. + Build and run tests to validate your branch. +3. Push the branch. +4. Create a PR, if one does not already exist. diff --git a/.agents/skills/update-library-template/SKILL.md b/.agents/skills/update-library-template/SKILL.md new file mode 100644 index 000000000..941df2f2f --- /dev/null +++ b/.agents/skills/update-library-template/SKILL.md @@ -0,0 +1,73 @@ +--- +name: update-library-template +description: Merges the latest Library.Template into this repo (at position of HEAD) and resolves conflicts. +disable-model-invocation: true +--- + +# Instructions + +1. Run `./tools/MergeFrom-Template.ps1` from the repo root. +2. Resolve merge conflicts, taking into account conflict resolution policy below. +3. Validate the changes, as described in the validation section below. +4. Committing your changes (if applicable). + +## Conflict resolution policy + +There may be [special notes](template-release-notes.md) that describe special considerations for certain files or scenarios to help you resolve conflicts appropriately. +Always refer to that file before proceeding. +In particular, focus on the *incoming* part of the file, since it represents the changes from the Library.Template that you are merging into your repo. + +Also consider that some repos choose to reject certain Library.Template patterns. +For example the template uses MTPv2 for test projects, but a repo might have chosen not to adopt that. +When resolving merge conflicts, consider whether it looks like the relevant code file is older than it should be given the changes the template is bringing in. +Ask the user when in doubt as to whether the conflict should be resolved in favor of 'catching up' with the template or keeping the current changes. + +Use #runSubagent to analyze and resolve merge conflicts across files in parallel. + +### Keep Current files + +Conflicts in the following files should always be resolved by keeping the current version (i.e. discard incoming changes): + +* README.md + +### Deleted files + +Very typically, when the incoming change is to a file that was deleted locally, the correct resolution is to re-delete the file. + +In some cases however, the deleted file may have incoming changes that should be applied to other files. +The `test/Library.Tests/Library.Tests.csproj` file is very typical of this. +Changes to this file should very typically be applied to any and all test projects in the repo. +You are responsible for doing this in addition to re-deleting this template file. + +## Updating package and SDK versions + +After the merge, always check global.json for MSBuild Sdks with names starting with `Microsoft.VisualStudio.Internal.MicroBuild`. +These SDK versions should match the value of the `MicroBuildVersion` property found in `Directory.Packages.props`. +Always take the latest of the versions you see among these SDKs and the `MicroBuildVersion` property. + +## Validation + +Validate the merge result (after resolving any conflicts, if applicable). +Use #runSubagent for each step. + +1. Verify that `dotnet restore` succeeds. Fix any issues that come up. +2. Verify that `dotnet build` succeeds. +3. Verify that tests succeed by running `tools/dotnet-test-cloud.ps1`. + +While these validations are described using `dotnet` CLI commands, some repos require using full msbuild.exe. +You can detect this by checking the `azure-pipelines/dotnet.yml` or `.github/workflows/build.yml` files for use of one or the other tool. + +You are *not* responsible for fixing issues that the merge did not cause. +If validation fails for reasons that seem unrelated to the changes brought in by the merge, advise the user and ask how they'd like you to proceed. +That said, sometimes merges will bring in SDK or dependency updates that can cause breaks in seemingly unrelated areas. +In such cases, you should investigate and solve the issues as needed. + +## Committing your changes + +If you have to make any changes for validations to pass, consider whether they qualify as a bad merge conflict resolution or more of a novel change that you're making to work with the Library.Template update. +Merge conflict resolution fixes ideally get amended into the merge commit, while novel changes would go into a novel commit after the merge commit. + +Always author your commits using `git commit --author "🤖 Copilot "` (and possibly other parameters). +Describe the nature of the merge conflicts you encountered and how you resolved them in your commit message. + +Later, if asked to review pull request validation breaks, always author a fresh commit with each fix that you push, unless the user directs you to do otherwise. diff --git a/.agents/skills/update-library-template/template-release-notes.md b/.agents/skills/update-library-template/template-release-notes.md new file mode 100644 index 000000000..975da315e --- /dev/null +++ b/.agents/skills/update-library-template/template-release-notes.md @@ -0,0 +1,20 @@ +# Template release notes + +This file will describe significant changes in Library.Template as they are introduced, especially if they require special consideration when merging updates into existing repos. +This file is referenced by update-library-template.prompt.md and should remain in place to facilitate future merges, whether done manually or by AI. + +## Solution rename + +Never leave a Library.slnx file in the repository. +You might even see one there even though this particular merge didn't bring it in. +This can be an artifact of having renamed Library.sln to Library.slnx in the template repo, but ultimately the receiving repo should have only one .sln or .slnx file, with a better name than `Library`. +Delete any `Library.slnx` that you see. +Migrate an `.sln` in the repo root to `.slnx` using this command: + +```ps1 +dotnet solution EXISTING.sln migrate +``` + +This will create an EXISTING.slnx file. `git add` that file, then `git rm` the old `.sln` file. +Sometimes a repo will reference the sln filename in a script or doc somewhere. +Search the repo for such references and update them to the slnx file. diff --git a/.azuredevops/dependabot.yml b/.azuredevops/dependabot.yml new file mode 100644 index 000000000..4d848fb58 --- /dev/null +++ b/.azuredevops/dependabot.yml @@ -0,0 +1,9 @@ +# Please see the documentation for all configuration options: +# https://eng.ms/docs/products/dependabot/configuration/version_updates + +version: 2 +updates: +- package-ecosystem: nuget + directory: / + schedule: + interval: monthly diff --git a/.config/1espt/PipelineAutobaseliningConfig.yml b/.config/1espt/PipelineAutobaseliningConfig.yml new file mode 100644 index 000000000..746296617 --- /dev/null +++ b/.config/1espt/PipelineAutobaseliningConfig.yml @@ -0,0 +1,68 @@ +## DO NOT MODIFY THIS FILE MANUALLY. This is part of auto-baselining from 1ES Pipeline Templates. Go to [https://aka.ms/1espt-autobaselining] for more details. + +pipelines: + 8123: + retail: + source: + credscan: + lastModifiedDate: 2024-09-06 + eslint: + lastModifiedDate: 2026-05-14 + armory: + lastModifiedDate: 2026-05-14 + policheck: + lastModifiedDate: 2026-05-14 + psscriptanalyzer: + lastModifiedDate: 2026-05-14 + accessibilityinsights: + lastModifiedDate: 2025-06-21 + binary: + credscan: + lastModifiedDate: 2024-09-06 + binskim: + lastModifiedDate: 2026-05-14 + spotbugs: + lastModifiedDate: 2026-05-14 + usedNonDefaultBranch: true + 13635: + retail: + source: + credscan: + lastModifiedDate: 2024-09-07 + eslint: + lastModifiedDate: 2024-09-07 + psscriptanalyzer: + lastModifiedDate: 2024-09-07 + armory: + lastModifiedDate: 2024-09-07 + accessibilityinsights: + lastModifiedDate: 2025-06-21 + 17763: + retail: + source: + credscan: + lastModifiedDate: 2025-01-13 + eslint: + lastModifiedDate: 2025-01-13 + psscriptanalyzer: + lastModifiedDate: 2025-01-13 + armory: + lastModifiedDate: 2025-01-13 + accessibilityinsights: + lastModifiedDate: 2025-07-13 + 25079: + retail: + source: + credscan: + lastModifiedDate: 2025-01-17 + eslint: + lastModifiedDate: 2025-01-17 + armory: + lastModifiedDate: 2025-01-17 + accessibilityinsights: + lastModifiedDate: 2025-06-20 + binary: + credscan: + lastModifiedDate: 2025-01-17 + binskim: + lastModifiedDate: 2025-01-17 diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 000000000..56c4ec6e7 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,48 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "powershell": { + "version": "7.6.4", + "commands": [ + "pwsh" + ], + "rollForward": false + }, + "dotnet-coverage": { + "version": "18.9.0", + "commands": [ + "dotnet-coverage" + ], + "rollForward": false + }, + "nbgv": { + "version": "3.10.91", + "commands": [ + "nbgv" + ], + "rollForward": false + }, + "docfx": { + "version": "2.78.5", + "commands": [ + "docfx" + ], + "rollForward": false + }, + "nerdbank.dotnetrepotools": { + "version": "1.5.15", + "commands": [ + "repo" + ], + "rollForward": false + }, + "dotnet-symbol": { + "version": "9.0.661903", + "commands": [ + "dotnet-symbol" + ], + "rollForward": false + } + } +} diff --git a/.config/guardian/.gdnbaselines b/.config/guardian/.gdnbaselines new file mode 100644 index 000000000..ab8c543bd --- /dev/null +++ b/.config/guardian/.gdnbaselines @@ -0,0 +1,72 @@ +{ + "properties": { + "helpUri": "https://eng.ms/docs/microsoft-security/security/azure-security/cloudai-security-fundamentals-engineering/security-integration/guardian-wiki/microsoft-guardian/general/baselines" + }, + "version": "1.0.0", + "baselines": { + "default": { + "name": "default", + "createdDate": "2026-05-14 03:47:36Z", + "lastUpdatedDate": "2026-05-14 03:47:36Z" + } + }, + "results": { + "dbff8dc971433ca7c818c690a4ae28b107c7e546bc335b049fabe2e160f0e628": { + "signature": "dbff8dc971433ca7c818c690a4ae28b107c7e546bc335b049fabe2e160f0e628", + "alternativeSignatures": [ + "c02350748c2f2ca142c3e8ae36ede02fda37ded67cb625a18294826debb8b022", + "b66593a9d8a961d03e7a676fc73a7c92acf9752f6030227a1867dc4b16280108", + "efb2054ac15706638108be5009009b5ba37343dce3caa258a39db002c4b88b9f" + ], + "target": "src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD110ObserveResultOfAsyncCallsAnalyzer.cs", + "line": 130, + "uriBaseId": "file:///D:/a/_work/1/s/", + "memberOf": [ + "default" + ], + "tool": "roslynanalyzers", + "ruleId": "CA1310", + "createdDate": "2026-05-14 03:47:36Z", + "expirationDate": "2026-10-31 04:34:32Z", + "justification": "This error is baselined with an expiration date of 180 days from 2026-05-14 04:34:32Z" + }, + "958ce3ce50b885fffd9057ceb48c553e916907958a3e8d423a3f79a0e9388708": { + "signature": "958ce3ce50b885fffd9057ceb48c553e916907958a3e8d423a3f79a0e9388708", + "alternativeSignatures": [ + "3f4623ce40b316b0f163abc6400237af661fc4b06722531df8d09350c1f2eddc", + "ad182cab3408ce61b793d348de8b5a1732b19094d97231d000e6a0ac2c873a15", + "066d02f45b528967d832e064f7c0ee32e8edd8c0dce2d19da340d916baa39c92" + ], + "target": "test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskTokenTests.cs", + "line": 61, + "uriBaseId": "file:///D:/a/_work/1/s/", + "memberOf": [ + "default" + ], + "tool": "roslynanalyzers", + "ruleId": "CA1307", + "createdDate": "2026-05-14 03:47:36Z", + "expirationDate": "2026-10-31 04:34:32Z", + "justification": "This error is baselined with an expiration date of 180 days from 2026-05-14 04:34:32Z" + }, + "d42f90bce28d3afe46faac67d711461f29995ee74a396bc9298f743af6c3bed6": { + "signature": "d42f90bce28d3afe46faac67d711461f29995ee74a396bc9298f743af6c3bed6", + "alternativeSignatures": [ + "fe52ee2339bc08550c0f5f481b6ad5abdd2b454d4ac85937dbd9e1b84f17156e", + "d688160bab0f20b29f2c7d0c04997973a608a9ce5e4222ee39142cdea765bd5a", + "fd005193dd710be4c2450f024a68c79f2be97321323c35e043d9c282cade492c" + ], + "target": "test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskTokenTests.cs", + "line": 194, + "uriBaseId": "file:///D:/a/_work/1/s/", + "memberOf": [ + "default" + ], + "tool": "roslynanalyzers", + "ruleId": "CA1307", + "createdDate": "2026-05-14 03:47:36Z", + "expirationDate": "2026-10-31 04:34:32Z", + "justification": "This error is baselined with an expiration date of 180 days from 2026-05-14 04:34:32Z" + } + } +} \ No newline at end of file diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index c47acfaa9..000000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM mcr.microsoft.com/dotnet/sdk:6.0.100-focal - -# Installing mono makes `dotnet test` work without errors even for net472. -# But installing it takes a long time, so it's excluded by default. -#RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF -#RUN echo "deb https://download.mono-project.com/repo/ubuntu stable-bionic main" | tee /etc/apt/sources.list.d/mono-official-stable.list -#RUN apt-get update -#RUN DEBIAN_FRONTEND=noninteractive apt-get install -y mono-devel - -# Clear the NUGET_XMLDOC_MODE env var so xml api doc files get unpacked, allowing a rich experience in Intellisense. -# See https://github.com/dotnet/dotnet-docker/issues/2790 for a discussion on this, where the prioritized use case -# was *not* devcontainers, sadly. -ENV NUGET_XMLDOC_MODE= diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index f4e3b31a3..000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Dev space", - "dockerFile": "Dockerfile", - "settings": { - "terminal.integrated.shell.linux": "/usr/bin/pwsh" - }, - "postCreateCommand": "./init.ps1 -InstallLocality machine", - "extensions": [ - "ms-azure-devops.azure-pipelines", - "ms-dotnettools.csharp", - "k--kato.docomment", - "editorconfig.editorconfig", - "pflannery.vscode-versionlens", - "davidanson.vscode-markdownlint", - "dotjoshjohnson.xml", - "ms-vscode-remote.remote-containers", - "ms-azuretools.vscode-docker", - "ms-vscode.powershell" - ] -} diff --git a/.editorconfig b/.editorconfig index 30189506e..cbb4859c2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -19,23 +19,29 @@ indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true -# Xml project files -[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,msbuildproj}] +# MSBuild project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,msbuildproj,props,targets}] indent_size = 2 # Xml config files -[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct,runsettings}] +[*.{ruleset,config,nuspec,resx,vsixmanifest,vsct,runsettings}] indent_size = 2 +indent_style = space # JSON files [*.json] indent_size = 2 indent_style = space +[*.ps1] +indent_style = space +indent_size = 4 + # Dotnet code style settings: [*.{cs,vb}] # Sort using and Import directives with System.* appearing first dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false dotnet_style_qualification_for_field = true:warning dotnet_style_qualification_for_property = true:warning dotnet_style_qualification_for_method = true:warning @@ -196,6 +202,20 @@ dotnet_diagnostic.CSIsNull001.severity = warning # CSIsNull002: Use `is object` for non-null checks dotnet_diagnostic.CSIsNull002.severity = warning +dotnet_diagnostic.DOC100.severity = silent +dotnet_diagnostic.DOC104.severity = warning +dotnet_diagnostic.DOC105.severity = warning +dotnet_diagnostic.DOC106.severity = warning +dotnet_diagnostic.DOC107.severity = warning +dotnet_diagnostic.DOC108.severity = warning +dotnet_diagnostic.DOC200.severity = warning +dotnet_diagnostic.DOC202.severity = warning + +# CA1062: Validate arguments of public methods +dotnet_diagnostic.CA1062.severity = warning + +# CA2016: Forward the CancellationToken parameter +dotnet_diagnostic.CA2016.severity = warning [*.sln] indent_style = tab diff --git a/.gitattributes b/.gitattributes index c22a129ef..1f35e683d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,6 +7,9 @@ *.sh eol=lf *.ps1 eol=lf +# The macOS codesign tool is extremely picky, and requires LF line endings. +*.plist eol=lf + ############################################################################### # Set default behavior for command prompt diff. # diff --git a/.github/.editorconfig b/.github/.editorconfig new file mode 100644 index 000000000..2b7682efb --- /dev/null +++ b/.github/.editorconfig @@ -0,0 +1,2 @@ +[renovate.json*] +indent_style = tab diff --git a/.github/Prime-ForCopilot.ps1 b/.github/Prime-ForCopilot.ps1 new file mode 100644 index 000000000..e0b1fb79d --- /dev/null +++ b/.github/Prime-ForCopilot.ps1 @@ -0,0 +1,5 @@ +if ((git -C $PSScriptRoot rev-parse --is-shallow-repository) -eq 'true') +{ + Write-Host "Shallow clone detected, disabling NBGV Git engine so the build can succeed." + $env:NBGV_GitEngine='Disabled' +} diff --git a/.github/actions/publish-artifacts/action.yaml b/.github/actions/publish-artifacts/action.yaml new file mode 100644 index 000000000..87b2f9c03 --- /dev/null +++ b/.github/actions/publish-artifacts/action.yaml @@ -0,0 +1,60 @@ +name: Publish artifacts +description: Publish artifacts + +runs: + using: composite + steps: + - name: 📥 Collect artifacts + run: tools/artifacts/_stage_all.ps1 + shell: pwsh + if: always() + +# TODO: replace this hard-coded list with a loop that utilizes the NPM package at +# https://github.com/actions/toolkit/tree/main/packages/artifact (or similar) to push the artifacts. + + - name: 📢 Upload project.assets.json files + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: projectAssetsJson-${{ runner.os }} + path: ${{ runner.temp }}/_artifacts/projectAssetsJson + continue-on-error: true + - name: 📢 Upload variables + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: variables-${{ runner.os }} + path: ${{ runner.temp }}/_artifacts/Variables + continue-on-error: true + - name: 📢 Upload build_logs + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: build_logs-${{ runner.os }} + path: ${{ runner.temp }}/_artifacts/build_logs + continue-on-error: true + - name: 📢 Upload testResults + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: testResults-${{ runner.os }} + path: ${{ runner.temp }}/_artifacts/testResults + continue-on-error: true + - name: 📢 Upload coverageResults + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: coverageResults-${{ runner.os }} + path: ${{ runner.temp }}/_artifacts/coverageResults + continue-on-error: true + - name: 📢 Upload symbols + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: symbols-${{ runner.os }} + path: ${{ runner.temp }}/_artifacts/symbols + continue-on-error: true + - name: 📢 Upload deployables + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: deployables-${{ runner.os }} + path: ${{ runner.temp }}/_artifacts/deployables + if: always() diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index f743dc361..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,12 +0,0 @@ -# Please see the documentation for all configuration options: -# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates - -version: 2 -updates: -- package-ecosystem: nuget - directory: / - schedule: - interval: monthly - ignore: - - dependency-name: Microsoft.CodeAnalysis* # We intentionally target older VS versions. - - dependency-name: DllExport # Later versions don't offer what we want. diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 000000000..d5154a48d --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "github>microsoft/vs-renovate-presets:microbuild", + "github>microsoft/vs-renovate-presets:vs_main_dependencies", + "github>microsoft/vs-renovate-presets:dotnet_packages_LTS", + "github>microsoft/vs-renovate-presets:xunitv2" + ], + "packageRules": [ + { + "matchFileNames": ["Directory.Packages.Analyzers.props"], + "enabled": false + } + ], + "customManagers": [ + { + "customType": "regex", + "datasourceTemplate": "nuget", + "managerFilePatterns": [ + "test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Helpers/ReferencesHelper.cs" + ], + "matchStrings": [ + "PackageIdentity\\(\"(?[^\"]+)\", \"(?[^\"]+)\"\\)" + ] + } + ] +} diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 000000000..71adf9e73 --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,41 @@ +name: 💪🏼 Copilot Setup Steps + +# Automatically run the setup steps when they are changed to allow for easy validation, and +# allow manual testing through the repository's "Actions" tab +on: + workflow_dispatch: + push: + branches: + - main + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. + copilot-setup-steps: + runs-on: ubuntu-latest + # Set the permissions to the lowest permissions possible needed for your steps. + # Copilot will be given its own token for its operations. + permissions: + # If you want to clone the repository as part of your setup steps, for example to install dependencies, you'll need the `contents: read` permission. If you don't clone the repository in your setup steps, Copilot will do this for you automatically after the steps complete. + contents: read + + # You can define any steps you want, and they will run before the agent starts. + # If you do not check out your code, Copilot will do this for you. + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 # avoid shallow clone so nbgv can do its work. + - name: ⚙ Install prerequisites + run: | + ./init.ps1 -UpgradePrerequisites -NoNuGetCredProvider + dotnet --info + + # Print mono version if it is present. + if (Get-Command mono -ErrorAction SilentlyContinue) { + mono --version + } + shell: pwsh diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..c462a0898 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,43 @@ +name: 📚 Docs + +on: + push: + branches: + - main + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + publish-docs: + # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages + permissions: + actions: read + pages: write + id-token: write + contents: read + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 # avoid shallow clone so nbgv can do its work. + - name: ⚙ Install prerequisites + run: ./init.ps1 -UpgradePrerequisites -NoNuGetCredProvider + + - run: dotnet docfx docfx/docfx.json + name: 📚 Generate documentation + + - name: Upload artifact + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + with: + path: docfx/_site + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 diff --git a/.github/workflows/docs_validate.yml b/.github/workflows/docs_validate.yml new file mode 100644 index 000000000..9b53d52c0 --- /dev/null +++ b/.github/workflows/docs_validate.yml @@ -0,0 +1,29 @@ +name: 📃 Docfx Validate + +on: + pull_request: + workflow_dispatch: + push: + branches: + - main + - microbuild + +jobs: + build: + name: 📚 Doc validation + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 # avoid shallow clone so nbgv can do its work. + - name: 🔗 Markup Link Checker (mlc) + uses: becheran/mlc@7ec24825cefe0c9c8c6bac48430e1f69e3ec356e # v1.2.0 + with: + args: --do-not-warn-for-redirect-to https://learn.microsoft.com*,https://dotnet.microsoft.com/*,https://dev.azure.com/*,https://app.codecov.io/* -p docfx -i https://aka.ms/onboardsupport,https://aka.ms/spot,https://msrc.microsoft.com/*,https://www.microsoft.com/msrc*,https://microsoft.com/msrc*,https://www.npmjs.com/package/*,https://get.dot.net/ + - name: ⚙ Install prerequisites + run: | + ./init.ps1 -UpgradePrerequisites + dotnet --info + shell: pwsh + - name: 📚 Verify docfx build + run: dotnet docfx docfx/docfx.json --warningsAsErrors --disableGitFeatures diff --git a/.github/workflows/libtemplate-update.yml b/.github/workflows/libtemplate-update.yml new file mode 100644 index 000000000..af1a9eef8 --- /dev/null +++ b/.github/workflows/libtemplate-update.yml @@ -0,0 +1,98 @@ +name: ⛜ Library.Template update + +# PREREQUISITE: This workflow requires the repo to be configured to allow workflows to create pull requests. +# Visit https://github.com/USER/REPO/settings/actions +# Under "Workflow permissions" check "Allow GitHub Actions to create ...pull requests" +# Click Save. + +on: + schedule: + - cron: "0 3 * * Mon" # Sun @ 8 or 9 PM Mountain Time (depending on DST) + workflow_dispatch: + +jobs: + merge: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 # avoid shallow clone so nbgv can do its work. + + - name: merge + id: merge + shell: pwsh + run: | + $LibTemplateBranch = & ./tools/Get-LibTemplateBasis.ps1 -ErrorIfNotRelated + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + + git fetch https://github.com/aarnott/Library.Template $LibTemplateBranch + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + $LibTemplateCommit = git rev-parse FETCH_HEAD + git diff --stat ...FETCH_HEAD + + if ((git rev-list FETCH_HEAD ^HEAD --count) -eq 0) { + Write-Host "There are no Library.Template updates to merge." + echo "uptodate=true" >> $env:GITHUB_OUTPUT + exit 0 + } + + # Pushing commits that add or change files under .github/workflows will cause our workflow to fail. + # But it usually isn't necessary because the target branch already has (or doesn't have) these changes. + # So if the merge doesn't bring in any changes to these files, try the merge locally and push that + # to keep github happy. + if ((git rev-list FETCH_HEAD ^HEAD --count -- .github/workflows) -eq 0) { + # Indeed there are no changes in that area. So merge locally to try to appease GitHub. + git checkout -b auto/libtemplateUpdate + git config user.name "Andrew Arnott" + git config user.email "andrewarnott@live.com" + git merge FETCH_HEAD + if ($LASTEXITCODE -ne 0) { + Write-Host "Merge conflicts prevent creating the pull request. Please run tools/MergeFrom-Template.ps1 locally and push the result as a pull request." + exit 2 + } + + git -c http.extraheader="AUTHORIZATION: bearer $env:GH_TOKEN" push origin -u HEAD + } else { + Write-Host "Changes to github workflows are included in this update. Please run tools/MergeFrom-Template.ps1 locally and push the result as a pull request." + exit 1 + } + - name: pull request + shell: pwsh + if: success() && steps.merge.outputs.uptodate != 'true' + run: | + # If there is already an active pull request, don't create a new one. + $existingPR = gh pr list -H auto/libtemplateUpdate --json url | ConvertFrom-Json + if ($existingPR) { + Write-Host "::warning::Skipping pull request creation because one already exists at $($existingPR[0].url)" + exit 0 + } + + $prTitle = "Merge latest Library.Template" + $prBody = "This merges the latest features and fixes from [Library.Template's branch](https://github.com/AArnott/Library.Template/tree/). + + ⚠️ Do **not** squash this pull request when completing it. You must *merge* it. + +
+ Merge conflicts? + Resolve merge conflicts locally by carrying out these steps: + + ``` + git fetch + git checkout auto/libtemplateUpdate + git merge origin/main + # resolve conflicts + git commit + git push + ``` +
" + + gh pr create -H auto/libtemplateUpdate -b $prBody -t $prTitle + env: + GH_TOKEN: ${{ github.token }} diff --git a/.gitignore b/.gitignore index d060a300b..83b1d55e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore # User-specific files *.rsuser @@ -9,6 +9,8 @@ *.user *.userosscache *.sln.docstates +*.lutconfig +launchSettings.json # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs @@ -35,6 +37,9 @@ bld/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ +# Jetbrains Rider cache directory +.idea/ + # Visual Studio 2017 auto generated files Generated\ Files/ @@ -138,6 +143,7 @@ _TeamCity* # Visual Studio code coverage results *.coverage *.coveragexml +/coveragereport/ # NCrunch _NCrunch_* @@ -346,3 +352,12 @@ MigrationBackup/ # dotnet tool local install directory .store/ + +# mac-created file to track user view preferences for a directory +.DS_Store + +# Analysis results +*.sarif + +# C# Dev Kit cache files +*.lscache diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/build/AdditionalFiles/vs-threading.MembersRequiringMainThread.txt b/.prettierrc.yaml similarity index 100% rename from src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/build/AdditionalFiles/vs-threading.MembersRequiringMainThread.txt rename to .prettierrc.yaml diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 4ca016163..acaf02131 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -7,11 +7,13 @@ "ms-dotnettools.csharp", "k--kato.docomment", "editorconfig.editorconfig", + "esbenp.prettier-vscode", "pflannery.vscode-versionlens", "davidanson.vscode-markdownlint", "dotjoshjohnson.xml", "ms-vscode-remote.remote-containers", - "ms-azuretools.vscode-docker" + "ms-azuretools.vscode-docker", + "tintoy.msbuild-project-tools" ], // List of extensions recommended by VS Code that should not be recommended for users of this workspace. "unwantedRecommendations": [] diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 000000000..cbe47099a --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,8 @@ +{ + "servers": { + "github": { + "url": "https://api.githubcopilot.com/mcp/" + } + }, + "inputs": [] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 3ae1371c6..a7bab44d1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,24 @@ "files.trimTrailingWhitespace": true, "files.insertFinalNewline": true, "files.trimFinalNewlines": true, + "azure-pipelines.1ESPipelineTemplatesSchemaFile": true, "omnisharp.enableEditorConfigSupport": true, - "omnisharp.enableImportCompletion": true, - "omnisharp.enableRoslynAnalyzers": true + "omnisharp.enableRoslynAnalyzers": true, + "dotnet.completion.showCompletionItemsFromUnimportedNamespaces": true, + "dotnet.defaultSolution": "Microsoft.VisualStudio.Threading.slnx", + "editor.formatOnSave": true, + "[xml]": { + "editor.wordWrap": "off" + }, + // Treat these files as Azure Pipelines files + "files.associations": { + "**/azure-pipelines/**/*.yml": "azure-pipelines", + "azure-pipelines.yml": "azure-pipelines" + }, + // Use Prettier as the default formatter for Azure Pipelines files. + // Needs to be explicitly configured: https://github.com/Microsoft/azure-pipelines-vscode#document-formatting + "[azure-pipelines]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": false // enable this when they conform + }, } diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..387894689 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,87 @@ +# Copilot instructions for this repository + +## High level guidance + +* Review the `CONTRIBUTING.md` file for instructions to build and test the software. +* Run the `.github/Prime-ForCopilot.ps1` script (once) before running any `dotnet` or `msbuild` commands. + If you see any build errors about not finding git objects or a shallow clone, it may be time to run this script again. + +## Software Design + +* Design APIs to be highly testable, and all functionality should be tested. +* Avoid introducing binary breaking changes in public APIs of projects under `src` unless their project files have `IsPackable` set to `false`. +* `InternalsVisibleTo` attributes are *not allowed*. + +## Testing + +**IMPORTANT**: This repository uses Microsoft.Testing.Platform (MTP v2) with xunit v3. Traditional `--filter` syntax does NOT work. Use the options below instead. + +* There should generally be one test project (under the `test` directory) per shipping project (under the `src` directory). Test projects are named after the project being tested with a `.Tests` suffix. +* Tests use xunit v3 with Microsoft.Testing.Platform (MTP v2). Traditional VSTest `--filter` syntax does NOT work. +* Some tests are known to be unstable. When running tests, you should skip the unstable ones by using `-- --filter-not-trait "FailsInCloudTest=true"`. +* Since `InternalsVisibleTo` is not allowed, testing must be done at the public API level. + In rare cases where there are static utility methods that need to be thoroughly tested, which may be impossible or inefficient to do via public APIs, the static methods may be moved to a .cs file that is then linked both into the product and into the test project so that it may be tested directly. + +### Running Tests + +**Run all tests**: +```bash +dotnet test --no-build -c Release +``` + +**Run tests for a specific test project**: +```bash +dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release +``` + +**Run a single test method**: +```bash +dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --filter-method ClassName.MethodName +``` + +**Run all tests in a test class**: +```bash +dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --filter-class ClassName +``` + +**Run tests with wildcard matching** (supports wildcards at beginning and/or end): +```bash +dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --filter-method "*Pattern*" +``` + +**Run tests with a specific trait** (equivalent to category filtering): +```bash +dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --filter-trait "TraitName=value" +``` + +**Exclude tests with a specific trait** (skip unstable tests): +```bash +dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --filter-not-trait "TestCategory=FailsInCloudTest" +``` + +**Run tests for a specific framework only**: +```bash +dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release --framework net9.0 +``` + +**List all available tests without running them**: +```bash +cd test/Library.Tests +dotnet run --no-build -c Release --framework net9.0 -- --list-tests +``` + +**Key points about test filtering with MTP v2 / xunit v3**: +- Options after `--` are passed to the test runner, not to `dotnet test` +- Use `--filter-method`, `--filter-class`, `--filter-namespace` for simple filtering +- Use `--filter-trait` and `--filter-not-trait` for trait-based filtering (replaces `--filter "TestCategory=..."`) +- Traditional VSTest `--filter` expressions do NOT work +- Wildcards `*` are supported at the beginning and/or end of filter values +- Multiple simple filters of the same type use OR logic, different types combine with AND +- See `--help` for query filter language for advanced scenarios + +## Coding style + +* Honor StyleCop rules and fix any reported build warnings *after* getting tests to pass. +* In C# files, use namespace *statements* instead of namespace *blocks* for all new files that define namespaces. +* Test files are *not* expected to declare namespaces. +* Add API doc comments to all new public and internal members. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9a266c193..05e4d16b1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,24 +7,98 @@ FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. -We welcome 3rd party pull requests. -For significant changes we strongly recommend opening an issue to start a design discussion first. +## Best practices + +* Use Windows PowerShell or [PowerShell Core][pwsh] (including on Linux/OSX) to run .ps1 scripts. + Some scripts set environment variables to help you, but they are only retained if you use PowerShell as your shell. + +## Prerequisites + +All dependencies can be installed by running the `init.ps1` script at the root of the repository +using Windows PowerShell or [PowerShell Core][pwsh] (on any OS). +Some dependencies installed by `init.ps1` may only be discoverable from the same command line environment the init script was run from due to environment variables, so be sure to launch Visual Studio or build the repo from that same environment. +Alternatively, run `init.ps1 -InstallLocality Machine` (which may require elevation) in order to install dependencies at machine-wide locations so Visual Studio and builds work everywhere. + +The only prerequisite for building, testing, and deploying from this repository +is the [.NET SDK](https://get.dot.net/). +You should install the version specified in `global.json` or a later version within +the same major.minor.Bxx "hundreds" band. +For example if 2.2.300 is specified, you may install 2.2.300, 2.2.301, or 2.2.310 +while the 2.2.400 version would not be considered compatible by .NET SDK. +See [.NET Core Versioning](https://learn.microsoft.com/dotnet/core/versions/) for more information. + +## Package restore + +The easiest way to restore packages may be to run `init.ps1` which automatically authenticates +to the feeds that packages for this repo come from, if any. +`dotnet restore` or `nuget restore` also work but may require extra steps to authenticate to any applicable feeds. ## Building -### Prerequisites +This repository can be built on Windows, Linux, and OSX. + +Building, testing, and packing this repository can be done by using the standard dotnet CLI commands (e.g. `dotnet build`, `dotnet test`, `dotnet pack`, etc.). + +## Testing + +You can use `dotnet test` to build and/or test the repo. + +There may be tests that are known to be unstable or have special requirements. These can be avoided by running tests using the [dotnet-test-cloud.ps1](tools/dotnet-test-cloud.ps1) script *after* running `dotnet build`. + +## Releases + +Use `nbgv tag` to create a tag for a particular commit that you mean to release. +[Learn more about `nbgv` and its `tag` and `prepare-release` commands](https://dotnet.github.io/Nerdbank.GitVersioning/docs/nbgv-cli.html). + +Push the tag. + +### GitHub Actions + +When your repo is hosted by GitHub and you are using GitHub Actions, you should create a GitHub Release using the standard GitHub UI. +Having previously used `nbgv tag` and pushing the tag will help you identify the precise commit and name to use for this release. + +After publishing the release, the `.github/workflows/release.yml` workflow will be automatically triggered, which will: + +1. Find the most recent `.github/workflows/build.yml` GitHub workflow run of the tagged release. +1. Upload the `deployables` artifact from that workflow run to your GitHub Release. +1. If you have `NUGET_API_KEY` defined as a secret variable for your repo or org, any nuget packages in the `deployables` artifact will be pushed to nuget.org. + +### Azure Pipelines + +When your repo builds with Azure Pipelines, use the `azure-pipelines/release.yml` pipeline. +Trigger the pipeline by adding the `auto-release` tag on a run of your main `azure-pipelines.yml` pipeline. + +## Tutorial and API documentation + +API and hand-written docs are found under the `docfx/` directory and are built by [docfx](https://dotnet.github.io/docfx/). + +You can make changes and host the site locally to preview them by switching to that directory and running the `dotnet docfx --serve` command. +After making a change, you can rebuild the docs site while the localhost server is running by running `dotnet docfx` again from a separate terminal. + +The `.github/workflows/docs.yml` GitHub Actions workflow publishes the content of these docs to github.io if the workflow itself and [GitHub Pages is enabled for your repository](https://docs.github.com/en/pages/quickstart). + +## Updating dependencies + +This repo uses Renovate to keep dependencies current. +Configuration is in the `.github/renovate.json` file. +[Learn more about configuring Renovate](https://docs.renovatebot.com/configuration-options/). + +When changing the renovate.json file, follow [these validation steps](https://docs.renovatebot.com/config-validation/). + +If Renovate is not creating pull requests when you expect it to, check that the [Renovate GitHub App](https://github.com/apps/renovate) is configured for your account or repo. -* [.NET Core SDK](https://dotnet.microsoft.com/download/dotnet-core/2.2) with the version matching our [global.json](global.json) file. The version you install must be at least the version specified in the global.json file, and must be within the same hundreds version for the 3rd integer: x.y.Czz (x.y.C must match, and zz must be at least as high). - The easiest way to get this is to run the `init` script at the root of the repo. Use the `-InstallLocality Machine` and approve admin elevation if you wish so the SDK is always discoverable from VS. See the `init` script usage doc for more details. -* Optional: [Visual Studio 2019](https://www.visualstudio.com/) +## Merging latest from Library.Template -### Build steps +### Maintaining your repo based on this template -This project can be built with the follow commands from a Visual Studio Developer Command Prompt, -assuming the working directory is the root of this repository: +The best way to keep your repo in sync with Library.Template's evolving features and best practices is to periodically merge the template into your repo: ```ps1 -msbuild src +git fetch +git checkout origin/main +./tools/MergeFrom-Template.ps1 +# resolve any conflicts, then commit the merge commit. +git push origin -u HEAD ``` -This solution can also be built from within Visual Studio 2019. +[pwsh]: https://learn.microsoft.com/powershell/scripting/install/installing-powershell diff --git a/CodeQL.yml b/CodeQL.yml new file mode 100644 index 000000000..903500b55 --- /dev/null +++ b/CodeQL.yml @@ -0,0 +1,3 @@ +path_classifiers: + library: + - 'test/**' diff --git a/Directory.Build.props b/Directory.Build.props index 8197ed724..62f74ed37 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,3 +1,4 @@ + Debug @@ -5,41 +6,57 @@ $(RepoRootPath)obj\$([MSBuild]::MakeRelative($(RepoRootPath), $(MSBuildProjectDirectory)))\ $(RepoRootPath)bin\$(MSBuildProjectName)\ $(RepoRootPath)bin\Packages\$(Configuration)\NuGet\ - 10.0 + $(RepoRootPath)bin\Packages\$(Configuration)\Vsix\$(Platform)\ + $(RepoRootPath)bin\Packages\$(Configuration)\Vsix\ + $(VSIXOutputPath) enable disable latest true true true + true - - $(MSBuildThisFileDirectory) + + true + + + true + + + true embedded + https://microsoft.github.io/vs-threading/ Microsoft Microsoft © Microsoft Corporation. All rights reserved. MIT - https://github.com/Microsoft/vs-threading true true true snupkg + - 2.0.66 + + 14 + 16.9 - - - - - - - - + + win + linux + osx + + x64 + arm64 + + $(RidOsPrefix)-$(RidOsArchitecture) + + $(DefaultRuntimeIdentifier) + @@ -56,34 +73,9 @@ - https://github.com/microsoft/vs-threading/releases/tag/v$(Version) + $(RepositoryUrl)/releases/tag/v$(Version) - - false - true - - - - - <_WpfTempProjectNuGetFilePathNoExt>$(RepoRootPath)obj\$(_TargetAssemblyProjectName)\$(_TargetAssemblyProjectName)$(MSBuildProjectExtension).nuget.g - - false - false - false - false - - - - + diff --git a/Directory.Build.targets b/Directory.Build.targets index 65a15bfc8..70909045c 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,8 +1,11 @@ + - - - false - + + + + - + + + diff --git a/Directory.Packages.Analyzers.props b/Directory.Packages.Analyzers.props new file mode 100644 index 000000000..a5770f019 --- /dev/null +++ b/Directory.Packages.Analyzers.props @@ -0,0 +1,21 @@ + + + + 4.11.0 + + + + + + + + + + + + + + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 000000000..600605280 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,64 @@ + + + + + true + true + 2.3.3 + 2.0.226 + 5.6.0 + 1.1.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Directory.Traversal.targets b/Directory.Traversal.targets new file mode 100644 index 000000000..805bd6e92 --- /dev/null +++ b/Directory.Traversal.targets @@ -0,0 +1,15 @@ + + + true + + false + false + true + true + + + + + + diff --git a/Microsoft.VisualStudio.Threading.sln b/Microsoft.VisualStudio.Threading.sln deleted file mode 100644 index 6e6704560..000000000 --- a/Microsoft.VisualStudio.Threading.sln +++ /dev/null @@ -1,130 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28413.118 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.Threading.Analyzers", "src\Microsoft.VisualStudio.Threading.Analyzers\Microsoft.VisualStudio.Threading.Analyzers.csproj", "{536F3F9A-B457-43B8-BC93-CE1C16959037}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.Threading.Analyzers.Tests", "test\Microsoft.VisualStudio.Threading.Analyzers.Tests\Microsoft.VisualStudio.Threading.Analyzers.Tests.csproj", "{620ED702-B6DA-4454-BF3E-5494D3652724}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{AA091F9F-B40A-466F-B09F-22EAD115996F}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - azure-pipelines\build.yml = azure-pipelines\build.yml - Directory.Build.props = Directory.Build.props - global.json = global.json - nuget.config = nuget.config - stylecop.json = stylecop.json - version.json = version.json - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.Threading.Tests.Win7RegistryWatcher", "test\Microsoft.VisualStudio.Threading.Tests.Win7RegistryWatcher\Microsoft.VisualStudio.Threading.Tests.Win7RegistryWatcher.csproj", "{4961AA84-088C-46C0-BAC0-F9E87A9F03A7}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.Threading", "src\Microsoft.VisualStudio.Threading\Microsoft.VisualStudio.Threading.csproj", "{D9BB9FB6-3833-44E8-B7A7-DE729FCE214D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.Threading.Tests", "test\Microsoft.VisualStudio.Threading.Tests\Microsoft.VisualStudio.Threading.Tests.csproj", "{CBEDB102-ABAE-40B1-AF3F-A6226DB6713D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IsolatedTestHost", "test\IsolatedTestHost\IsolatedTestHost.csproj", "{BA4643D8-E6B2-4DED-882F-4827F3AB6AB0}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.Threading.Analyzers.CodeFixes", "src\Microsoft.VisualStudio.Threading.Analyzers.CodeFixes\Microsoft.VisualStudio.Threading.Analyzers.CodeFixes.csproj", "{3BDB8F46-A39C-422B-8B0E-89E98B83073F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SosThreadingTools", "src\SosThreadingTools\SosThreadingTools.csproj", "{7177DEEE-D14D-4A4A-BF6E-8B0CDC26B624}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.Threading.Analyzers.CSharp", "src\Microsoft.VisualStudio.Threading.Analyzers.CSharp\Microsoft.VisualStudio.Threading.Analyzers.CSharp.csproj", "{D5A0D627-7853-43F5-9AF4-E23D062C6ABA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.Threading.Analyzers.VisualBasic", "src\Microsoft.VisualStudio.Threading.Analyzers.VisualBasic\Microsoft.VisualStudio.Threading.Analyzers.VisualBasic.csproj", "{8CDF7526-D625-4E16-A266-BAF654ABE181}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|NonWindows = Debug|NonWindows - Release|Any CPU = Release|Any CPU - Release|NonWindows = Release|NonWindows - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {536F3F9A-B457-43B8-BC93-CE1C16959037}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {536F3F9A-B457-43B8-BC93-CE1C16959037}.Debug|Any CPU.Build.0 = Debug|Any CPU - {536F3F9A-B457-43B8-BC93-CE1C16959037}.Debug|NonWindows.ActiveCfg = Debug|Any CPU - {536F3F9A-B457-43B8-BC93-CE1C16959037}.Debug|NonWindows.Build.0 = Debug|Any CPU - {536F3F9A-B457-43B8-BC93-CE1C16959037}.Release|Any CPU.ActiveCfg = Release|Any CPU - {536F3F9A-B457-43B8-BC93-CE1C16959037}.Release|Any CPU.Build.0 = Release|Any CPU - {536F3F9A-B457-43B8-BC93-CE1C16959037}.Release|NonWindows.ActiveCfg = Release|Any CPU - {536F3F9A-B457-43B8-BC93-CE1C16959037}.Release|NonWindows.Build.0 = Release|Any CPU - {620ED702-B6DA-4454-BF3E-5494D3652724}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {620ED702-B6DA-4454-BF3E-5494D3652724}.Debug|Any CPU.Build.0 = Debug|Any CPU - {620ED702-B6DA-4454-BF3E-5494D3652724}.Debug|NonWindows.ActiveCfg = Debug|Any CPU - {620ED702-B6DA-4454-BF3E-5494D3652724}.Debug|NonWindows.Build.0 = Debug|Any CPU - {620ED702-B6DA-4454-BF3E-5494D3652724}.Release|Any CPU.ActiveCfg = Release|Any CPU - {620ED702-B6DA-4454-BF3E-5494D3652724}.Release|Any CPU.Build.0 = Release|Any CPU - {620ED702-B6DA-4454-BF3E-5494D3652724}.Release|NonWindows.ActiveCfg = Release|Any CPU - {620ED702-B6DA-4454-BF3E-5494D3652724}.Release|NonWindows.Build.0 = Release|Any CPU - {4961AA84-088C-46C0-BAC0-F9E87A9F03A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4961AA84-088C-46C0-BAC0-F9E87A9F03A7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4961AA84-088C-46C0-BAC0-F9E87A9F03A7}.Debug|NonWindows.ActiveCfg = Debug|Any CPU - {4961AA84-088C-46C0-BAC0-F9E87A9F03A7}.Debug|NonWindows.Build.0 = Debug|Any CPU - {4961AA84-088C-46C0-BAC0-F9E87A9F03A7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4961AA84-088C-46C0-BAC0-F9E87A9F03A7}.Release|Any CPU.Build.0 = Release|Any CPU - {4961AA84-088C-46C0-BAC0-F9E87A9F03A7}.Release|NonWindows.ActiveCfg = Release|Any CPU - {4961AA84-088C-46C0-BAC0-F9E87A9F03A7}.Release|NonWindows.Build.0 = Release|Any CPU - {D9BB9FB6-3833-44E8-B7A7-DE729FCE214D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D9BB9FB6-3833-44E8-B7A7-DE729FCE214D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D9BB9FB6-3833-44E8-B7A7-DE729FCE214D}.Debug|NonWindows.ActiveCfg = Debug|Any CPU - {D9BB9FB6-3833-44E8-B7A7-DE729FCE214D}.Debug|NonWindows.Build.0 = Debug|Any CPU - {D9BB9FB6-3833-44E8-B7A7-DE729FCE214D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D9BB9FB6-3833-44E8-B7A7-DE729FCE214D}.Release|Any CPU.Build.0 = Release|Any CPU - {D9BB9FB6-3833-44E8-B7A7-DE729FCE214D}.Release|NonWindows.ActiveCfg = Release|Any CPU - {D9BB9FB6-3833-44E8-B7A7-DE729FCE214D}.Release|NonWindows.Build.0 = Release|Any CPU - {CBEDB102-ABAE-40B1-AF3F-A6226DB6713D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CBEDB102-ABAE-40B1-AF3F-A6226DB6713D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CBEDB102-ABAE-40B1-AF3F-A6226DB6713D}.Debug|NonWindows.ActiveCfg = Debug|Any CPU - {CBEDB102-ABAE-40B1-AF3F-A6226DB6713D}.Debug|NonWindows.Build.0 = Debug|Any CPU - {CBEDB102-ABAE-40B1-AF3F-A6226DB6713D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CBEDB102-ABAE-40B1-AF3F-A6226DB6713D}.Release|Any CPU.Build.0 = Release|Any CPU - {CBEDB102-ABAE-40B1-AF3F-A6226DB6713D}.Release|NonWindows.ActiveCfg = Release|Any CPU - {CBEDB102-ABAE-40B1-AF3F-A6226DB6713D}.Release|NonWindows.Build.0 = Release|Any CPU - {BA4643D8-E6B2-4DED-882F-4827F3AB6AB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BA4643D8-E6B2-4DED-882F-4827F3AB6AB0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BA4643D8-E6B2-4DED-882F-4827F3AB6AB0}.Debug|NonWindows.ActiveCfg = Debug|Any CPU - {BA4643D8-E6B2-4DED-882F-4827F3AB6AB0}.Debug|NonWindows.Build.0 = Debug|Any CPU - {BA4643D8-E6B2-4DED-882F-4827F3AB6AB0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BA4643D8-E6B2-4DED-882F-4827F3AB6AB0}.Release|Any CPU.Build.0 = Release|Any CPU - {BA4643D8-E6B2-4DED-882F-4827F3AB6AB0}.Release|NonWindows.ActiveCfg = Release|Any CPU - {BA4643D8-E6B2-4DED-882F-4827F3AB6AB0}.Release|NonWindows.Build.0 = Release|Any CPU - {3BDB8F46-A39C-422B-8B0E-89E98B83073F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3BDB8F46-A39C-422B-8B0E-89E98B83073F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3BDB8F46-A39C-422B-8B0E-89E98B83073F}.Debug|NonWindows.ActiveCfg = Debug|Any CPU - {3BDB8F46-A39C-422B-8B0E-89E98B83073F}.Debug|NonWindows.Build.0 = Debug|Any CPU - {3BDB8F46-A39C-422B-8B0E-89E98B83073F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3BDB8F46-A39C-422B-8B0E-89E98B83073F}.Release|Any CPU.Build.0 = Release|Any CPU - {3BDB8F46-A39C-422B-8B0E-89E98B83073F}.Release|NonWindows.ActiveCfg = Release|Any CPU - {3BDB8F46-A39C-422B-8B0E-89E98B83073F}.Release|NonWindows.Build.0 = Release|Any CPU - {7177DEEE-D14D-4A4A-BF6E-8B0CDC26B624}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7177DEEE-D14D-4A4A-BF6E-8B0CDC26B624}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7177DEEE-D14D-4A4A-BF6E-8B0CDC26B624}.Debug|NonWindows.ActiveCfg = Debug|Any CPU - {7177DEEE-D14D-4A4A-BF6E-8B0CDC26B624}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7177DEEE-D14D-4A4A-BF6E-8B0CDC26B624}.Release|Any CPU.Build.0 = Release|Any CPU - {7177DEEE-D14D-4A4A-BF6E-8B0CDC26B624}.Release|NonWindows.ActiveCfg = Release|Any CPU - {D5A0D627-7853-43F5-9AF4-E23D062C6ABA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D5A0D627-7853-43F5-9AF4-E23D062C6ABA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D5A0D627-7853-43F5-9AF4-E23D062C6ABA}.Debug|NonWindows.ActiveCfg = Debug|Any CPU - {D5A0D627-7853-43F5-9AF4-E23D062C6ABA}.Debug|NonWindows.Build.0 = Debug|Any CPU - {D5A0D627-7853-43F5-9AF4-E23D062C6ABA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D5A0D627-7853-43F5-9AF4-E23D062C6ABA}.Release|Any CPU.Build.0 = Release|Any CPU - {D5A0D627-7853-43F5-9AF4-E23D062C6ABA}.Release|NonWindows.ActiveCfg = Release|Any CPU - {D5A0D627-7853-43F5-9AF4-E23D062C6ABA}.Release|NonWindows.Build.0 = Release|Any CPU - {8CDF7526-D625-4E16-A266-BAF654ABE181}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8CDF7526-D625-4E16-A266-BAF654ABE181}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8CDF7526-D625-4E16-A266-BAF654ABE181}.Debug|NonWindows.ActiveCfg = Debug|Any CPU - {8CDF7526-D625-4E16-A266-BAF654ABE181}.Debug|NonWindows.Build.0 = Debug|Any CPU - {8CDF7526-D625-4E16-A266-BAF654ABE181}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8CDF7526-D625-4E16-A266-BAF654ABE181}.Release|Any CPU.Build.0 = Release|Any CPU - {8CDF7526-D625-4E16-A266-BAF654ABE181}.Release|NonWindows.ActiveCfg = Release|Any CPU - {8CDF7526-D625-4E16-A266-BAF654ABE181}.Release|NonWindows.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {E2124DFF-970E-4BA1-9E50-3ADB0AABF347} - EndGlobalSection -EndGlobal diff --git a/Microsoft.VisualStudio.Threading.slnx b/Microsoft.VisualStudio.Threading.slnx new file mode 100644 index 000000000..647f3cb99 --- /dev/null +++ b/Microsoft.VisualStudio.Threading.slnx @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index d1e841e86..cabe45111 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,22 @@ -Microsoft.VisualStudio.Threading -================================= +# vs-threading -[![NuGet package](https://img.shields.io/nuget/v/Microsoft.VisualStudio.Threading.svg)](https://nuget.org/packages/Microsoft.VisualStudio.Threading) [![Build Status](https://dev.azure.com/azure-public/vside/_apis/build/status/vs-threading)](https://dev.azure.com/azure-public/vside/_build/latest?definitionId=12) [![Join the chat at https://gitter.im/vs-threading/Lobby](https://badges.gitter.im/vs-threading/Lobby.svg)](https://gitter.im/vs-threading/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -Analyzers: [![NuGet package](https://img.shields.io/nuget/v/Microsoft.VisualStudio.Threading.Analyzers.svg)](https://nuget.org/packages/Microsoft.VisualStudio.Threading.Analyzers) - -## Features - -* Async versions of many threading synchronization primitives - * `AsyncAutoResetEvent` - * `AsyncBarrier` - * `AsyncCountdownEvent` - * `AsyncManualResetEvent` - * `AsyncReaderWriterLock` - * `AsyncSemaphore` - * `ReentrantSemaphore` -* Async versions of very common types - * `AsyncEventHandler` - * `AsyncLazy` - * `AsyncLazyInitializer` - * `AsyncLocal` - * `AsyncQueue` -* Await extension methods - * Await on a `TaskScheduler` to switch to it. - Switch to a background thread with `await TaskScheduler.Default;` - * Await on a `Task` with a timeout - * Await on a `Task` with cancellation -* `JoinableTaskFactory` that allows you to schedule asynchronous or synchronous work - that does not deadlock with the UI thread even when the UI thread needs to - synchronously block on the result. - -## Documentation - -* [Overview documentation](doc/index.md) -* [Diagnostic analyzer rules](doc/analyzers/index.md) - -## Supported platforms - -* .NET 4.5 -* .NET 4.6 -* .NET Standard 1.3 -* .NET Standard 2.0 - -[1]: https://nuget.org/packages/Microsoft.VisualStudio.Threading "Microsoft.VisualStudio.Threading NuGet package" +## Microsoft.VisualStudio.Threading + +[![NuGet package](https://img.shields.io/nuget/v/Microsoft.VisualStudio.Threading.svg)](https://www.nuget.org/packages/Microsoft.VisualStudio.Threading) + +Async synchronization primitives, async collections, TPL and dataflow extensions. The JoinableTaskFactory allows synchronously blocking the UI thread for async work. This package is applicable to any .NET application (not just Visual Studio). + +[Getting started](https://microsoft.github.io/vs-threading/docs/getting-started.html). + +[See the full list of features](https://microsoft.github.io/vs-threading/docs/features.html). + +## Microsoft.VisualStudio.Threading.Analyzers + +[![NuGet package](https://img.shields.io/nuget/v/Microsoft.VisualStudio.Threading.Analyzers.svg)](https://www.nuget.org/packages/Microsoft.VisualStudio.Threading.Analyzers) + +Static code analyzer to detect common mistakes or potential issues regarding threading and async coding. + +[Diagnostic analyzer rules](https://microsoft.github.io/vs-threading/analyzers/index.html). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..29306956d --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://learn.microsoft.com/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/msrc/pgp-key-msrc). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/msrc/cvd). + + diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 000000000..0f876b3ed --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,16 @@ +# Support + +## How to file issues and get help + +This project uses GitHub Issues to track bugs and feature requests. +Please search the existing issues before filing new issues to avoid duplicates. +For new issues, file your bug or feature request as a new Issue. + +Note that this repo is primarily used for Visual Studio and related products and support will be focused on those scenarios. + +## Microsoft Support Policy + +Microsoft support for this software is available only for its use in officially supported products such as Visual Studio. +Support and servicing is limited to the latest released version. +For more information, see [Visual Studio Product Lifecycle and Servicing](https://learn.microsoft.com/visualstudio/productinfo/vs-servicing). +Assisted support is available from a professional support engineer by opening a ticket with the [Microsoft assisted support team](https://support.serviceshub.microsoft.com/supportforbusiness/onboarding). diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 37d1a574f..5091c5131 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -3,7 +3,7 @@ trigger: branches: include: - main - - 'v*' + - 'v*.*' - 'validate/*' paths: exclude: @@ -11,23 +11,24 @@ trigger: - '*.md' - .vscode/ - .github/ + - azure-pipelines/release.yml parameters: -- name: includeMacOS +- name: EnableMacOSBuild displayName: Build on macOS type: boolean default: false # macOS is often bogged down in Azure Pipelines +- name: RunTests + displayName: Run tests + type: boolean + default: true variables: - TreatWarningsAsErrors: true - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true - BuildConfiguration: Release - # codecov_token: 4dc9e7e2-6b01-4932-a180-847b52b43d35 # Get a new one from https://codecov.io/ - NUGET_PACKAGES: $(Agent.TempDirectory)/.nuget/packages +- template: /azure-pipelines/BuildStageVariables.yml@self jobs: - template: azure-pipelines/build.yml parameters: - includeMacOS: ${{ parameters.includeMacOS }} - ShouldSkipOptimize: -- template: azure-pipelines/richnav.yml + Is1ESPT: false + EnableMacOSBuild: ${{ parameters.EnableMacOSBuild }} + RunTests: ${{ parameters.RunTests }} diff --git a/azure-pipelines/Archive-SourceCode.ps1 b/azure-pipelines/Archive-SourceCode.ps1 new file mode 100644 index 000000000..0360a14fa --- /dev/null +++ b/azure-pipelines/Archive-SourceCode.ps1 @@ -0,0 +1,234 @@ +#Requires -PSEdition Core -Version 7 +<# +.SYNOPSIS + Submits a source archival request for this repo. +.PARAMETER Requester + The alias for the user requesting this backup. +.PARAMETER ManagerAlias + The alias of the manager that owns the repo. +.PARAMETER TeamAlias + The alias of the team that owns the repo. +.PARAMETER BusinessGroupName + A human-readable title for your team or business group. +.PARAMETER ProductionType +.PARAMETER ReleaseType + The type of release being backed up. +.PARAMETER ReleaseDate + The date of the release of your software. Defaults to today. +.PARAMETER OwnerAlias + The alias of the owner. +.PARAMETER OS +.PARAMETER ProductLanguage + One or more languages. +.PARAMETER Notes + Any notes to record with the backup. +.PARAMETER FileCollection + One or more collections to archive. +.PARAMETER ProductName + The name of the product. This will default to the repository name. +.PARAMETER RepoUrl + The URL to the repository. This will default to the repository containing this script. +.PARAMETER BackupType + The kind of backup to be performed. +.PARAMETER ServerPath + The UNC path to the server to be backed up (if applicable). +.PARAMETER SourceCodeArchivalUri + The URI to POST the source code archival request to. + This value will typically come automatically by a variable group associated with your pipeline. + You can also look it up at https://dpsopsrequestforms.azurewebsites.net/#/help -> SCA Request Help -> SCA API Help -> Description +#> +[CmdletBinding(SupportsShouldProcess = $true, PositionalBinding = $false)] +param ( + [Parameter()] + [string]$Requester, + [Parameter(Mandatory = $true)] + [string]$ManagerAlias, + [Parameter(Mandatory = $true)] + [string]$TeamAlias, + [Parameter(Mandatory = $true)] + [string]$BusinessGroupName, + [Parameter()] + [string]$ProductionType = 'Visual Studio', + [Parameter()] + [string]$ReleaseType = 'RTW', + [Parameter()] + [DateTime]$ReleaseDate = [DateTime]::Today, + [Parameter()] + [string]$OwnerAlias, + [Parameter()] + [ValidateSet('64-Bit Win', '32-Bit Win', 'Linux', 'Mac', '64-Bit ARM', '32-Bit ARM')] + [string[]]$OS = @('64-Bit Win'), + [Parameter(Mandatory = $true)] + [ValidateSet('English', 'Chinese Simplified', 'Chinese Traditional', 'Czech', 'French', 'German', 'Italian', 'Japanese', 'Korean', 'Polish', 'Portuguese', 'Russian', 'Spanish', 'Turkish')] + [string[]]$ProductLanguage, + [Parameter()] + [string]$Notes = '', + [Parameter()] + [ValidateSet('Binaries', 'Localization', 'Source Code')] + [string[]]$FileCollection = @('Source Code'), + [Parameter()] + [string]$ProductName, + [Parameter()] + [Uri]$RepoUrl, + [Parameter()] + [ValidateSet('Server Path', 'Code Repo(Git URL/AzureDevOps)', 'Git', 'Azure Storage Account')] + [string]$BackupType = 'Code Repo(Git URL/AzureDevOps)', + [Parameter()] + [string]$ServerPath = '', + [Parameter()] + [Uri]$SourceCodeArchivalUri = $env:SOURCECODEARCHIVALURI, + [Parameter(Mandatory = $true)] + [string]$AccessToken +) + +function Invoke-Git() { + # Make sure we invoke git from within the repo. + Push-Location $PSScriptRoot + try { + return (git $args) + } + finally { + Pop-Location + } +} + +if (!$ProductName) { + if ($env:BUILD_REPOSITORY_NAME) { + Write-Verbose 'Using $env:BUILD_REPOSITORY_NAME for ProductName.' # single quotes are intentional so user sees the name of env var. + $ProductName = $env:BUILD_REPOSITORY_NAME + } + else { + $originUrl = [Uri](Invoke-Git remote get-url origin) + if ($originUrl) { + $lastPathSegment = $originUrl.Segments[$originUrl.Segments.Length - 1] + if ($lastPathSegment.EndsWith('.git')) { + $lastPathSegment = $lastPathSegment.Substring(0, $lastPathSegment.Length - '.git'.Length) + } + Write-Verbose 'Using origin remote URL to derive ProductName.' + $ProductName = $lastPathSegment + } + } + + if (!$ProductName) { + Write-Error "Unable to determine default value for -ProductName." + } +} + +if (!$OwnerAlias) { + if ($env:BUILD_REQUESTEDFOREMAIL) { + Write-Verbose 'Using $env:BUILD_REQUESTEDFOREMAIL and slicing to just the alias for OwnerAlias.' + $OwnerAlias = ($env:BUILD_REQUESTEDFOREMAIL -split '@')[0] + } else { + $OwnerAlias = $TeamAlias + } + + if (!$OwnerAlias) { + Write-Error "Unable to determine default value for -OwnerAlias." + } +} + +if (!$Requester) { + if ($env:BUILD_REQUESTEDFOREMAIL) { + Write-Verbose 'Using $env:BUILD_REQUESTEDFOREMAIL and slicing to just the alias for Requester.' + $Requester = ($env:BUILD_REQUESTEDFOREMAIL -split '@')[0] + } + else { + Write-Verbose 'Using $env:USERNAME for Requester.' + $Requester = $env:USERNAME + } + if (!$Requester) { + $Requester = $OwnerAlias + } +} + +if (!$RepoUrl) { + $RepoUrl = $env:BUILD_REPOSITORY_URI + if (!$RepoUrl) { + $originUrl = [Uri](Invoke-Git remote get-url origin) + if ($originUrl) { + Write-Verbose 'Using git origin remote url for GitURL.' + $RepoUrl = $originUrl + } + + if (!$RepoUrl) { + Write-Error "Unable to determine default value for -RepoUrl." + } + } +} + +Push-Location $PSScriptRoot +$versionsObj = dotnet nbgv get-version -f json | ConvertFrom-Json +Pop-Location + +$ReleaseDateString = $ReleaseDate.ToShortDateString() +$Version = $versionsObj.Version + +$BackupSize = Get-ChildItem $PSScriptRoot\..\.git -Recurse -File | Measure-Object -Property Length -Sum +$DataSizeMB = [int]($BackupSize.Sum / 1mb) +$FileCount = $BackupSize.Count + +$Request = @{ + "Requester" = $Requester + "Manager" = $ManagerAlias + "TeamAlias" = $TeamAlias + "AdditionalContacts" = $AdditionalContacts + "BusinessGroupName" = $BusinessGroupName + "ProductName" = $ProductName + "Version" = $Version + "ProductionType" = $ProductionType + "ReleaseType" = $ReleaseType + "ReleaseDateString" = $ReleaseDateString + "OS" = [string]::Join(',', $OS) + "ProductLanguage" = [string]::Join(',', $ProductLanguage) + "FileCollection" = [string]::Join(',', $FileCollection) + "OwnerAlias" = $OwnerAlias + "Notes" = $Notes.Trim() + "CustomerProvidedDataSizeMB" = $DataSizeMB + "CustomerProvidedFileCount" = $FileCount + "BackupType" = $BackupType + "ServerPath" = $ServerPath + "AzureStorageAccount" = $AzureStorageAccount + "AzureStorageContainer" = $AzureStorageContainer + "GitURL" = $RepoUrl +} + +$RequestJson = ConvertTo-Json $Request +Write-Host "SCA request:`n$RequestJson" + +if ($PSCmdlet.ShouldProcess('source archival request', 'post')) { + if (!$SourceCodeArchivalUri) { + Write-Error "Unable to post request without -SourceCodeArchivalUri parameter." + exit 1 + } + + $headers = @{ + 'Authorization' = "Bearer $AccessToken" + } + + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + + $Response = Invoke-WebRequest -Uri $SourceCodeArchivalUri -Method POST -Headers $headers -Body $RequestJson -ContentType "application/json" -UseBasicParsing -SkipHttpErrorCheck + Write-Host "Status Code : " -NoNewline + if ($Response.StatusCode -eq 200) { + Write-Host $Response.StatusCode -ForegroundColor Green + Write-Host "Ticket ID : " -NoNewline + $responseContent = ConvertFrom-Json ($Response.Content) + Write-Host $responseContent + } + else { + Write-Host $Response.StatusCode -ForegroundColor Red + try { + $responseContent = ConvertFrom-Json $Response.Content + Write-Host "Message : $($responseContent.message)" + } + catch { + Write-Host "JSON Parse Error: $($_.Exception.Message)" + Write-Host "Raw response content:" + Write-Host $Response.Content + } + + exit 2 + } +} elseif ($SourceCodeArchivalUri) { + Write-Host "Would have posted to $SourceCodeArchivalUri" +} diff --git a/azure-pipelines/BuildStageVariables.yml b/azure-pipelines/BuildStageVariables.yml new file mode 100644 index 000000000..7c61f8fec --- /dev/null +++ b/azure-pipelines/BuildStageVariables.yml @@ -0,0 +1,4 @@ +variables: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + BuildConfiguration: Release + NUGET_PACKAGES: $(Agent.TempDirectory)/.nuget/packages/ diff --git a/azure-pipelines/Convert-PDB.ps1 b/azure-pipelines/Convert-PDB.ps1 deleted file mode 100644 index 2d394e72d..000000000 --- a/azure-pipelines/Convert-PDB.ps1 +++ /dev/null @@ -1,43 +0,0 @@ -<# -.SYNOPSIS - Converts between Windows PDB and Portable PDB formats. -.PARAMETER DllPath - The path to the DLL whose PDB is to be converted. -.PARAMETER PdbPath - The path to the PDB to convert. May be omitted if the DLL was compiled on this machine and the PDB is still at its original path. -.PARAMETER OutputPath - The path of the output PDB to write. -#> -#Function Convert-PortableToWindowsPDB() { - Param( - [Parameter(Mandatory=$true,Position=0)] - [string]$DllPath, - [Parameter()] - [string]$PdbPath, - [Parameter(Mandatory=$true,Position=1)] - [string]$OutputPath - ) - - if ($IsMacOS -or $IsLinux) { - Write-Error "This script only works on Windows" - return - } - - $version = '1.1.0-beta2-21101-01' - $baseDir = "$PSScriptRoot/../obj/tools" - $pdb2pdbpath = "$baseDir/Microsoft.DiaSymReader.Pdb2Pdb.$version/tools/Pdb2Pdb.exe" - if (-not (Test-Path $pdb2pdbpath)) { - if (-not (Test-Path $baseDir)) { New-Item -Type Directory -Path $baseDir | Out-Null } - $baseDir = (Resolve-Path $baseDir).Path # Normalize it - Write-Verbose "& (& $PSScriptRoot/Get-NuGetTool.ps1) install Microsoft.DiaSymReader.Pdb2Pdb -version $version -PackageSaveMode nuspec -OutputDirectory $baseDir -Source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json | Out-Null" - & (& $PSScriptRoot/Get-NuGetTool.ps1) install Microsoft.DiaSymReader.Pdb2Pdb -version $version -PackageSaveMode nuspec -OutputDirectory $baseDir -Source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json | Out-Null - } - - $args = $DllPath,'/out',$OutputPath,'/nowarn','0021' - if ($PdbPath) { - $args += '/pdb',$PdbPath - } - - Write-Verbose "$pdb2pdbpath $args" - & $pdb2pdbpath $args -#} diff --git a/azure-pipelines/Darwin.runsettings b/azure-pipelines/Darwin.runsettings deleted file mode 100644 index 4ad8e7066..000000000 --- a/azure-pipelines/Darwin.runsettings +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/azure-pipelines/Get-InsertionPRId.ps1 b/azure-pipelines/Get-InsertionPRId.ps1 new file mode 100644 index 000000000..62cb30cd8 --- /dev/null +++ b/azure-pipelines/Get-InsertionPRId.ps1 @@ -0,0 +1,26 @@ +<# +.SYNOPSIS + Look up the pull request URL of the insertion PR. +#> +$stagingFolder = $env:BUILD_STAGINGDIRECTORY +if (!$stagingFolder) { + $stagingFolder = $env:SYSTEM_DEFAULTWORKINGDIRECTORY + if (!$stagingFolder) { + Write-Error "This script must be run in an Azure Pipeline." + exit 1 + } +} +$markdownFolder = Join-Path $stagingFolder (Join-Path 'MicroBuild' 'Output') +$markdownFile = Join-Path $markdownFolder 'PullRequestUrl.md' +if (!(Test-Path $markdownFile)) { + Write-Error "This script should be run after the MicroBuildInsertVsPayload task." + exit 2 +} + +$insertionPRUrl = Get-Content $markdownFile +if (!($insertionPRUrl -match 'https:.+?/pullrequest/(\d+)')) { + Write-Error "Failed to parse pull request URL: $insertionPRUrl" + exit 3 +} + +$Matches[1] diff --git a/azure-pipelines/Get-NuGetTool.ps1 b/azure-pipelines/Get-NuGetTool.ps1 deleted file mode 100644 index 4431adb91..000000000 --- a/azure-pipelines/Get-NuGetTool.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -<# -.SYNOPSIS - Downloads the NuGet.exe tool and returns the path to it. -.PARAMETER NuGetVersion - The version of the NuGet tool to acquire. -#> -Param( - [Parameter()] - [string]$NuGetVersion='5.2.0' -) - -$toolsPath = & "$PSScriptRoot\Get-TempToolsPath.ps1" -$binaryToolsPath = Join-Path $toolsPath $NuGetVersion -if (!(Test-Path $binaryToolsPath)) { $null = mkdir $binaryToolsPath } -$nugetPath = Join-Path $binaryToolsPath nuget.exe - -if (!(Test-Path $nugetPath)) { - Write-Host "Downloading nuget.exe $NuGetVersion..." -ForegroundColor Yellow - (New-Object System.Net.WebClient).DownloadFile("https://dist.nuget.org/win-x86-commandline/v$NuGetVersion/NuGet.exe", $nugetPath) -} - -return (Resolve-Path $nugetPath).Path diff --git a/azure-pipelines/Get-ProcDump.ps1 b/azure-pipelines/Get-ProcDump.ps1 deleted file mode 100644 index 1493fe4b2..000000000 --- a/azure-pipelines/Get-ProcDump.ps1 +++ /dev/null @@ -1,14 +0,0 @@ -<# -.SYNOPSIS -Downloads 32-bit and 64-bit procdump executables and returns the path to where they were installed. -#> -$version = '0.0.1' -$baseDir = "$PSScriptRoot\..\obj\tools" -$procDumpToolPath = "$baseDir\procdump.$version\bin" -if (-not (Test-Path $procDumpToolPath)) { - if (-not (Test-Path $baseDir)) { New-Item -Type Directory -Path $baseDir | Out-Null } - $baseDir = (Resolve-Path $baseDir).Path # Normalize it - & (& $PSScriptRoot\Get-NuGetTool.ps1) install procdump -version $version -PackageSaveMode nuspec -OutputDirectory $baseDir -Source https://api.nuget.org/v3/index.json | Out-Null -} - -(Resolve-Path $procDumpToolPath).Path diff --git a/azure-pipelines/Get-SymbolFiles.ps1 b/azure-pipelines/Get-SymbolFiles.ps1 deleted file mode 100644 index 8793e19c6..000000000 --- a/azure-pipelines/Get-SymbolFiles.ps1 +++ /dev/null @@ -1,75 +0,0 @@ -<# -.SYNOPSIS - Collect the list of PDBs built in this repo, after converting them from portable to Windows PDBs. -.PARAMETER Path - The root path to recursively search for PDBs. -.PARAMETER Tests - A switch indicating to find test-related PDBs instead of product-only PDBs. -.PARAMETER ConvertToWindowsPDBs - A switch to convert and return paths to Windows PDBs instead of portable PDBs. - Ignored on non-Windows agents. -#> -[CmdletBinding()] -param ( - [parameter(Mandatory=$true)] - [string]$Path, - [switch]$Tests, - [switch]$ConvertToWindowsPDBs=$true -) - -$WindowsPdbSubDirName = "symstore" - -$ActivityName = "Collecting symbols from $Path" -Write-Progress -Activity $ActivityName -CurrentOperation "Discovery PDB files" -$PDBs = Get-ChildItem -rec "$Path/*.pdb" |? { $_.FullName -notmatch "\W$WindowsPdbSubDirName\W" } - -# Filter PDBs to product OR test related. -$testregex = "unittest|tests" -if ($Tests) { - $PDBs = $PDBs |? { $_.FullName -match $testregex } -} else { - $PDBs = $PDBs |? { $_.FullName -notmatch $testregex } -} - -Write-Progress -Activity $ActivityName -CurrentOperation "De-duplicating symbols" -$PDBsByHash = @{} -$i = 0 -$PDBs |% { - Write-Progress -Activity $ActivityName -CurrentOperation "De-duplicating symbols" -PercentComplete (100 * $i / $PDBs.Length) - $hash = Get-FileHash $_ - $i++ - Add-Member -InputObject $_ -MemberType NoteProperty -Name Hash -Value $hash.Hash - Write-Output $_ -} | Sort-Object CreationTime |% { - # De-dupe based on hash. Prefer the first match so we take the first built copy. - if (-not $PDBsByHash.ContainsKey($_.Hash)) { - $PDBsByHash.Add($_.Hash, $_.FullName) - Write-Output $_ - } -} |% { - # Collect the DLLs/EXEs as well. - $dllPath = "$($_.Directory)/$($_.BaseName).dll" - $exePath = "$($_.Directory)/$($_.BaseName).exe" - if (Test-Path $dllPath) { - $BinaryImagePath = $dllPath - } elseif (Test-Path $exePath) { - $BinaryImagePath = $exePath - } - - Write-Output $BinaryImagePath - - if ($ConvertToWindowsPDBs -and -not ($IsMacOS -or $IsLinux)) { - # Convert the PDB to legacy Windows PDBs - Write-Host "Converting PDB for $_" -ForegroundColor DarkGray - $WindowsPdbDir = "$($_.Directory.FullName)\$WindowsPdbSubDirName" - if (!(Test-Path $WindowsPdbDir)) { mkdir $WindowsPdbDir | Out-Null } - & "$PSScriptRoot\Convert-PDB.ps1" -DllPath $BinaryImagePath -PdbPath $_ -OutputPath "$WindowsPdbDir\$($_.BaseName).pdb" - if ($LASTEXITCODE -ne 0) { - Write-Warning "PDB conversion of `"$_`" failed." - } - - Write-Output "$WindowsPdbDir\$($_.BaseName).pdb" - } else { - Write-Output $_.FullName - } -} diff --git a/azure-pipelines/Get-nbgv.ps1 b/azure-pipelines/Get-nbgv.ps1 deleted file mode 100644 index a5be2cf7c..000000000 --- a/azure-pipelines/Get-nbgv.ps1 +++ /dev/null @@ -1,24 +0,0 @@ -<# -.SYNOPSIS - Gets the path to the nbgv CLI tool, installing it if necessary. -#> -Param( -) - -$existingTool = Get-Command "nbgv" -ErrorAction SilentlyContinue -if ($existingTool) { - return $existingTool.Path -} - -$toolInstallDir = & "$PSScriptRoot/Get-TempToolsPath.ps1" - -$toolPath = "$toolInstallDir/nbgv" -if (!(Test-Path $toolInstallDir)) { New-Item -Path $toolInstallDir -ItemType Directory | Out-Null } - -if (!(Get-Command $toolPath -ErrorAction SilentlyContinue)) { - Write-Host "Installing nbgv to $toolInstallDir" - dotnet tool install --tool-path "$toolInstallDir" nbgv --configfile "$PSScriptRoot/justnugetorg.nuget.config" | Out-Null -} - -# Normalize the path on the way out. -return (Get-Command $toolPath).Path diff --git a/azure-pipelines/GlobalVariables.yml b/azure-pipelines/GlobalVariables.yml new file mode 100644 index 000000000..a4691dbb4 --- /dev/null +++ b/azure-pipelines/GlobalVariables.yml @@ -0,0 +1,6 @@ +variables: + # These variables are required for MicroBuild tasks + TeamName: VS Core - Special Projects + TeamEmail: andarno@microsoft.com + # These variables influence insertion pipelines + ContainsVsix: false # This should be true when the repo builds a VSIX that should be inserted to VS. diff --git a/azure-pipelines/InsertionMetadataPackage.nuspec b/azure-pipelines/InsertionMetadataPackage.nuspec deleted file mode 100644 index ac362c33e..000000000 --- a/azure-pipelines/InsertionMetadataPackage.nuspec +++ /dev/null @@ -1,16 +0,0 @@ - - - - Microsoft.VisualStudio.Threading.VSInsertionMetadata - $version$ - Microsoft - Microsoft - https://github.com/Microsoft/vs-threading - false - Contains metadata for insertion into VS. - © Microsoft Corporation. All rights reserved. - - - - - diff --git a/azure-pipelines/Install-NuGetPackage.ps1 b/azure-pipelines/Install-NuGetPackage.ps1 deleted file mode 100644 index 0bf057104..000000000 --- a/azure-pipelines/Install-NuGetPackage.ps1 +++ /dev/null @@ -1,50 +0,0 @@ -<# -.SYNOPSIS - Installs a NuGet package. -.PARAMETER PackageID - The Package ID to install. -.PARAMETER Version - The version of the package to install. If unspecified, the latest stable release is installed. -.PARAMETER Source - The package source feed to find the package to install from. -.PARAMETER PackagesDir - The directory to install the package to. By default, it uses the Packages folder at the root of the repo. -.PARAMETER ConfigFile - The nuget.config file to use. By default, it uses :/nuget.config. -#> -[CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Low')] -Param( - [Parameter(Position=1,Mandatory=$true)] - [string]$PackageId, - [Parameter()] - [string]$Version, - [Parameter()] - [string]$Source, - [Parameter()] - [switch]$Prerelease, - [Parameter()] - [string]$PackagesDir="$PSScriptRoot\..\packages", - [Parameter()] - [string]$ConfigFile="$PSScriptRoot\..\nuget.config", - [Parameter()] - [ValidateSet('Quiet','Normal','Detailed')] - [string]$Verbosity='normal' -) - -$nugetPath = & "$PSScriptRoot\Get-NuGetTool.ps1" - -try { - Write-Verbose "Installing $PackageId..." - $nugetArgs = "Install",$PackageId,"-OutputDirectory",$PackagesDir,'-ConfigFile',$ConfigFile - if ($Version) { $nugetArgs += "-Version",$Version } - if ($Source) { $nugetArgs += "-FallbackSource",$Source } - if ($Prerelease) { $nugetArgs += "-Prerelease" } - $nugetArgs += '-Verbosity',$Verbosity - - if ($PSCmdlet.ShouldProcess($PackageId, 'nuget install')) { - $p = Start-Process $nugetPath $nugetArgs -NoNewWindow -Wait -PassThru - if ($p.ExitCode -ne 0) { throw } - } -} finally { - Pop-Location -} diff --git a/azure-pipelines/Linux.runsettings b/azure-pipelines/Linux.runsettings deleted file mode 100644 index 4ad8e7066..000000000 --- a/azure-pipelines/Linux.runsettings +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/azure-pipelines/Merge-CodeCoverage.ps1 b/azure-pipelines/Merge-CodeCoverage.ps1 new file mode 100644 index 000000000..c552d25f2 --- /dev/null +++ b/azure-pipelines/Merge-CodeCoverage.ps1 @@ -0,0 +1,52 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Merges code coverage reports. +.PARAMETER Path + The path(s) to search for Cobertura code coverage reports. +.PARAMETER Format + The format for the merged result. The default is Cobertura +.PARAMETER OutputDir + The directory the merged result will be written to. The default is `coveragereport` in the root of this repo. +#> +[CmdletBinding()] +Param( + [Parameter(Mandatory=$true)] + [string[]]$Path, + [ValidateSet('Badges', 'Clover', 'Cobertura', 'CsvSummary', 'Html', 'Html_Dark', 'Html_Light', 'HtmlChart', 'HtmlInline', 'HtmlInline_AzurePipelines', 'HtmlInline_AzurePipelines_Dark', 'HtmlInline_AzurePipelines_Light', 'HtmlSummary', 'JsonSummary', 'Latex', 'LatexSummary', 'lcov', 'MarkdownSummary', 'MHtml', 'PngChart', 'SonarQube', 'TeamCitySummary', 'TextSummary', 'Xml', 'XmlSummary')] + [string]$Format='Cobertura', + [string]$OutputFile=("$PSScriptRoot/../coveragereport/merged.cobertura.xml") +) + +$RepoRoot = [string](Resolve-Path $PSScriptRoot/..) +Push-Location $RepoRoot +try { + Write-Verbose "Searching $Path for *.cobertura.xml files" + $reports = Get-ChildItem -Recurse $Path -Filter *.cobertura.xml + + if ($reports) { + $reports |% { $_.FullName } |% { + # In addition to replacing {reporoot}, we also normalize on one kind of slash so that the report aggregates data for a file whether data was collected on Windows or not. + Write-Verbose "Processing $_" + $xml = [xml](Get-Content -LiteralPath $_) + $xml.coverage.packages.package.classes.class |? { $_.filename} |% { + $_.filename = $_.filename.Replace('{reporoot}', $RepoRoot).Replace([IO.Path]::AltDirectorySeparatorChar, [IO.Path]::DirectorySeparatorChar) + } + + $xml.Save($_) + } + + $Inputs = $reports |% { Resolve-Path -relative $_.FullName } + + if ((Split-Path $OutputFile) -and -not (Test-Path (Split-Path $OutputFile))) { + New-Item -Type Directory -Path (Split-Path $OutputFile) | Out-Null + } + + & dotnet dotnet-coverage merge $Inputs -o $OutputFile -f cobertura + } else { + Write-Error "No reports found to merge." + } +} finally { + Pop-Location +} diff --git a/azure-pipelines/NuGetSbom.props b/azure-pipelines/NuGetSbom.props new file mode 100644 index 000000000..dbfee864b --- /dev/null +++ b/azure-pipelines/NuGetSbom.props @@ -0,0 +1,6 @@ + + + true + 2 + + diff --git a/azure-pipelines/OptProf.yml b/azure-pipelines/OptProf.yml deleted file mode 100644 index 6c7befb85..000000000 --- a/azure-pipelines/OptProf.yml +++ /dev/null @@ -1,117 +0,0 @@ -trigger: none -pr: none -schedules: - - cron: "0 3 * * Tue" # Mon @ 8 or 9 PM Mountain Time (depending on DST) - displayName: Weekly OptProf run - branches: - include: - - 'v1*' - - main - always: true # we must keep data fresh since optimizationdata drops are purged after 30 days - -# Avoid errant CI builds: https://developercommunity.visualstudio.com/content/problem/1154409/azure-pipeline-is-triggering-due-to-events-that-ne.html -#resources: -# repositories: -# - repository: scripts -# type: git -# name: DeploymentScripts -# ref: refs/heads/test - -variables: -- name: TreatWarningsAsErrors - value: true -- name: DOTNET_SKIP_FIRST_TIME_EXPERIENCE - value: true -- name: BuildConfiguration - value: Release -- name: BuildPlatform - value: Any CPU -- name: NUGET_PACKAGES - value: $(Agent.TempDirectory)/.nuget/packages -- name: PublicRelease - value: false # avoid using nice version since we're building a preliminary/unoptimized package -- name: SignType - value: real -- group: Library.Template -- name: NugetSecurityAnalysisWarningLevel - value: none # nuget.config requires signed packages by trusted owners - -stages: -- stage: Library - variables: - OptProf: true - jobs: - - template: build.yml - parameters: - windowsPool: VSEngSS-MicroBuild2022-1ES - includeMacOS: false - ShouldSkipOptimize: true -- stage: QueueVSBuild - jobs: - - job: QueueOptProf - pool: - vmImage: windows-latest - variables: - InsertPayloadName: vs-threading - InsertTopicBranch: team/VS-IDE/vs-threading-OptProf-run-$(Build.BuildId) - steps: - - checkout: none # We don't need source from our own repo - clean: true - - # Pipeline YAML does not yet support checking out other repos. So we'll do it by hand. -# - checkout: scripts # We DO need source from the DeploymentScripts repo -# clean: true -# path: $(Agent.TempDirectory)/DeploymentScripts -# fetchDepth: 1 - - script: 'git -c http.extraheader="AUTHORIZATION: bearer $(System.AccessToken)" clone https://devdiv.visualstudio.com/DevDiv/_git/DeploymentScripts --depth 1 --branch test "$(Agent.TempDirectory)/DeploymentScripts"' - displayName: Download DeploymentScripts repo - - - task: DownloadBuildArtifacts@0 - displayName: Download insertion artifacts - inputs: - artifactName: VSInsertion-Windows - downloadPath: $(Agent.TempDirectory) - - task: DownloadBuildArtifacts@0 - displayName: Download variables artifacts - inputs: - artifactName: Variables-Windows - downloadPath: $(Agent.TempDirectory) - - task: PowerShell@2 - displayName: Set pipeline variables based on artifacts - inputs: - targetType: filePath - filePath: $(Agent.TempDirectory)/Variables-Windows/_pipelines.ps1 - - task: NuGetCommand@2 - displayName: Push CoreXT packages to VS feed - inputs: - command: push - packagesToPush: $(Agent.TempDirectory)/VSInsertion-Windows/*.nupkg - publishVstsFeed: 97a41293-2972-4f48-8c0e-05493ae82010 # VS feed - allowPackageConflicts: true - - task: MicroBuildInsertVsPayload@3 - displayName: Insert VS Payload - inputs: - SkipCreatePR: true - CustomScriptExecutionCommand: src\VSSDK\NuGet\AllowUnstablePackages.ps1 - - task: benjhuser.tfs-extensions-build-tasks.trigger-build-task.TriggerBuild@3 - displayName: Trigger a new build of DD-CB-PR - inputs: - buildDefinition: DD-CB-PR - useSameBranch: false - branchToUse: $(InsertTopicBranch) - storeInEnvironmentVariable: true - queueBuildForUserThatTriggeredBuild: false - authenticationMethod: OAuth Token - password: $(System.AccessToken) - - task: PowerShell@2 - displayName: Associate InsertionOutputs artifacts with CloudBuild - inputs: - targetType: filePath - filePath: $(Agent.TempDirectory)/DeploymentScripts/Scripts/Insertion/WriteArtifact.ps1 - arguments: '-oldBuildID $(Build.BuildId) -newBuildID $(TriggeredBuildIds) -artifactName "InsertionOutputs" -accessToken $(System.AccessToken)' - - task: PowerShell@2 - displayName: Tag the build with vs-threading-insertion - inputs: - targetType: filePath - filePath: $(Agent.TempDirectory)/DeploymentScripts/Scripts/Insertion/TagBuild.ps1 - arguments: '-buildID $(TriggeredBuildIds) -tagName "vs-threading-insertion" -accessToken $(System.AccessToken)' diff --git a/azure-pipelines/PostPRMessage.ps1 b/azure-pipelines/PostPRMessage.ps1 new file mode 100644 index 000000000..4075f3921 --- /dev/null +++ b/azure-pipelines/PostPRMessage.ps1 @@ -0,0 +1,57 @@ +[CmdletBinding(SupportsShouldProcess = $true)] +param( + [Parameter(Mandatory=$true)] + $AccessToken, + [Parameter(Mandatory=$true)] + $Markdown, + [ValidateSet('Active','ByDesign','Closed','Fixed','Pending','Unknown','WontFix')] + $CommentState='Active' +) + +# See https://learn.microsoft.com/dotnet/api/microsoft.teamfoundation.sourcecontrol.webapi.commentthreadstatus +if ($CommentState -eq 'Active') { + $StatusCode = 1 +} elseif ($CommentState -eq 'ByDesign') { + $StatusCode = 5 +} elseif ($CommentState -eq 'Closed') { + $StatusCode = 4 +} elseif ($CommentState -eq 'Fixed') { + $StatusCode = 2 +} elseif ($CommentState -eq 'Pending') { + $StatusCode = 6 +} elseif ($CommentState -eq 'Unknown') { + $StatusCode = 0 +} elseif ($CommentState -eq 'WontFix') { + $StatusCode = 3 +} + +# Build the JSON body up +$body = ConvertTo-Json @{ + comments = @(@{ + parentCommentId = 0 + content = $Markdown + commentType = 1 + }) + status = $StatusCode +} + +Write-Verbose "Posting JSON payload: `n$Body" + +# Post the message to the Pull Request +# https://learn.microsoft.com/rest/api/azure/devops/git/pull-request-threads +$url = "$($env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI)$env:SYSTEM_TEAMPROJECTID/_apis/git/repositories/$($env:BUILD_REPOSITORY_NAME)/pullRequests/$($env:SYSTEM_PULLREQUEST_PULLREQUESTID)/threads?api-version=5.1" +if ($PSCmdlet.ShouldProcess($url, 'Post comment via REST call')) { + try { + if (!$env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI) { + Write-Error "Posting to the pull request requires that the script is running in an Azure Pipelines context." + exit 1 + } + Write-Host "Posting PR comment to: $url" + Invoke-RestMethod -Uri $url -Method POST -Headers @{Authorization = "Bearer $AccessToken"} -Body $Body -ContentType application/json + } + catch { + Write-Error $_ + Write-Error $_.Exception.Message + exit 2 + } +} diff --git a/azure-pipelines/ProfilingInputs.props b/azure-pipelines/ProfilingInputs.props deleted file mode 100644 index fb19d6048..000000000 --- a/azure-pipelines/ProfilingInputs.props +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/azure-pipelines/TSAOptions.json b/azure-pipelines/TSAOptions.json new file mode 100644 index 000000000..bee919ce6 --- /dev/null +++ b/azure-pipelines/TSAOptions.json @@ -0,0 +1,19 @@ +{ + "tsaVersion": "TsaV2", + "codebase": "NewOrUpdate", + "codebaseName": "Microsoft.VisualStudio.Threading", + "tsaStamp": "DevDiv", + "tsaEnvironment": "PROD", + "notificationAliases": [ + "andarno@microsoft.com" + ], + "codebaseAdmins": [ + "REDMOND\\andarno" + ], + "instanceUrl": "https://devdiv.visualstudio.com", + "projectName": "DevDiv", + "areaPath": "DevDiv\\VS Core\\Special Projects\\vs-threading", + "iterationPath": "DevDiv", + "alltools": true, + "repositoryName": "vs-threading" +} diff --git a/azure-pipelines/WIFtoPATauth.yml b/azure-pipelines/WIFtoPATauth.yml new file mode 100644 index 000000000..cb78f61f2 --- /dev/null +++ b/azure-pipelines/WIFtoPATauth.yml @@ -0,0 +1,22 @@ +parameters: +- name: deadPATServiceConnectionId # The GUID of the PAT-based service connection whose access token must be replaced. + type: string +- name: wifServiceConnectionName # The name of the WIF service connection to use to get the access token. + type: string +- name: resource # The scope for which the access token is requested. + type: string + default: 499b84ac-1321-427f-aa17-267ca6975798 # Azure Artifact feeds (any of them) + +steps: +- task: AzureCLI@2 + displayName: 🔏 Authenticate with WIF service connection + inputs: + azureSubscription: ${{ parameters.wifServiceConnectionName }} + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + $accessToken = az account get-access-token --query accessToken --resource '${{ parameters.resource }}' -o tsv + # Set the access token as a secret, so it doesn't get leaked in the logs + Write-Host "##vso[task.setsecret]$accessToken" + # Override the apitoken of the nuget service connection, for the duration of this stage + Write-Host "##vso[task.setendpoint id=${{ parameters.deadPATServiceConnectionId }};field=authParameter;key=apitoken]$accessToken" diff --git a/azure-pipelines/Windows_NT.runsettings b/azure-pipelines/Windows_NT.runsettings deleted file mode 100644 index 4ad8e7066..000000000 --- a/azure-pipelines/Windows_NT.runsettings +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/azure-pipelines/apiscan.yml b/azure-pipelines/apiscan.yml new file mode 100644 index 000000000..a1d07e104 --- /dev/null +++ b/azure-pipelines/apiscan.yml @@ -0,0 +1,63 @@ +parameters: +- name: windowsPool + type: object +- name: RealSign + type: boolean + +jobs: +- job: apiscan + displayName: APIScan + dependsOn: Windows + pool: ${{ parameters.windowsPool }} + timeoutInMinutes: 120 + templateContext: + ${{ if not(parameters.RealSign) }}: + mb: + signing: # if the build is test-signed, install the signing plugin so that CSVTestSignPolicy.xml is available + enabled: true + zipSources: false + signType: test + outputs: + - output: pipelineArtifact + displayName: 📢 collect apiscan artifact + targetPath: $(Pipeline.Workspace)/.gdn/.r/apiscan/001/Logs + artifactName: apiscan-logs + condition: succeededOrFailed() + variables: + - name: SymbolsFeatureName + value: $[ dependencies.Windows.outputs['SetPipelineVariables.SymbolsFeatureName'] ] + - name: NBGV_MajorMinorVersion + value: $[ dependencies.Windows.outputs['nbgv.NBGV_MajorMinorVersion'] ] + - ${{ if eq(variables['system.collectionId'], '011b8bdf-6d56-4f87-be0d-0092136884d9') }}: + # https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/25351/APIScan-step-by-step-guide-to-setting-up-a-Pipeline + - group: VSEng sponsored APIScan # Expected to provide ApiScanClientId + steps: + # We need TSAOptions.json + - checkout: self + fetchDepth: 1 + + - download: current + artifact: APIScanInputs + displayName: 🔻 Download APIScanInputs artifact + + - task: APIScan@2 + displayName: 🔍 Run APIScan + inputs: + softwareFolder: $(Pipeline.Workspace)/APIScanInputs + softwareName: $(SymbolsFeatureName) + softwareVersionNum: $(NBGV_MajorMinorVersion) + isLargeApp: false + toolVersion: Latest + preserveLogsFolder: true + azureSubscription: VSEng-APIScanSC + env: + AzureServicesAuthConnectionString: $(APIScanAuthConnectionString) + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + + # File bugs when APIScan finds issues + - task: TSAUpload@2 + displayName: 🪳 TSA upload + inputs: + GdnPublishTsaOnboard: True + GdnPublishTsaConfigFile: $(Build.SourcesDirectory)\azure-pipelines\TSAOptions.json + condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) diff --git a/azure-pipelines/archive-sourcecode.yml b/azure-pipelines/archive-sourcecode.yml new file mode 100644 index 000000000..cb8d68e9c --- /dev/null +++ b/azure-pipelines/archive-sourcecode.yml @@ -0,0 +1,90 @@ +trigger: none # We only want to trigger manually or based on resources +pr: none + +# Source archival requirements come from a compliance tenet. Review a sample task here: https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1550985 +# Source code should be archived within 30 days of the release date, and at least every quarter if your product is releasing more than once every 6 months. +# If your sources on GitHub are public open source project, then using GitHub Public Archive is sufficient. +schedules: +- cron: "13 13 13 */3 *" # Every three months + displayName: Periodic source archival + branches: + include: + - main + +resources: + repositories: + - repository: MicroBuildTemplate + type: git + name: 1ESPipelineTemplates/MicroBuildTemplate + ref: refs/tags/release + +parameters: +- name: notes + displayName: Notes to include in the SCA request + type: string + default: ' ' # optional parameters require a non-empty default. +- name: whatif + displayName: Only simulate the request + type: boolean + default: false + +variables: +- group: VS Core team # Expected to provide ManagerAlias, SourceCodeArchivalUri +- template: GlobalVariables.yml + +extends: + template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate + parameters: + settings: + networkIsolationPolicy: Permissive,CFSClean2 + sdl: + sourceAnalysisPool: VSEng-MicroBuildVSStable + + stages: + - stage: archive + jobs: + - job: archive + pool: + name: AzurePipelines-EO + demands: + - ImageOverride -equals 1ESPT-Ubuntu24.04 + os: Linux + + steps: + - checkout: self + clean: true + fetchDepth: 0 + - powershell: tools/Install-DotNetSdk.ps1 + displayName: ⚙ Install .NET SDK + - task: NuGetAuthenticate@1 + displayName: 🔏 Authenticate NuGet feeds + inputs: + forceReinstallCredentialProvider: true + - script: dotnet tool restore + displayName: ⚙️ Restore CLI tools + - powershell: tools/variables/_define.ps1 + failOnStderr: true + displayName: ⚙ Set pipeline variables based on source + - task: AzureCLI@2 + displayName: 🔏 Authenticate with WIF service connection + inputs: + azureSubscription: VS Core Source Code Archival + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + $accessToken = az account get-access-token --query accessToken --resource api://177cf50a-4bf5-4481-8b7e-f32900dfc8e6 -o tsv + Write-Host "##vso[task.setvariable variable=scaToken;issecret=true]$accessToken" + - pwsh: > + $TeamAlias = '$(TeamEmail)'.Substring(0, '$(TeamEmail)'.IndexOf('@')) + + azure-pipelines/Archive-SourceCode.ps1 + -ManagerAlias '$(ManagerAlias)' + -TeamAlias $TeamAlias + -BusinessGroupName '$(BusinessGroupName)' + -ProductName '$(SymbolsFeatureName)' + -ProductLanguage English + -Notes '${{ parameters.notes }}' + -AccessToken '$(scaToken)' + -Verbose + -WhatIf:$${{ parameters.whatif }} + displayName: 🗃️ Submit archival request diff --git a/azure-pipelines/artifacts/VSInsertion.ps1 b/azure-pipelines/artifacts/VSInsertion.ps1 deleted file mode 100644 index 0e848f3c2..000000000 --- a/azure-pipelines/artifacts/VSInsertion.ps1 +++ /dev/null @@ -1,45 +0,0 @@ -# This artifact captures everything needed to insert into VS (NuGet packages, insertion metadata, etc.) - -if ($IsMacOS -or $IsLinux) { - # We only package up for insertions on Windows agents since they are where optprof can happen. - Write-Verbose "Skipping VSInsertion artifact since we're not on Windows" - return @{} -} - -$RepoRoot = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..") -$config = 'Debug' -if ($env:BUILDCONFIGURATION) { $config = $env:BUILDCONFIGURATION } -$NuGetPackages = "$RepoRoot\bin\Packages\$config\NuGet" -$CoreXTPackages = "$RepoRoot\bin\Packages\$config\CoreXT" -if (-not (Test-Path $NuGetPackages)) { Write-Warning "No NuGet packages found. Has a build been run?"; return @{} } - -# This artifact is not ready if we're running on the devdiv AzDO account and we don't have an SBOM yet. -if ($env:SYSTEM_COLLECTIONID -eq '011b8bdf-6d56-4f87-be0d-0092136884d9' -and -not (Test-Path $NuGetPackages/_manifest)) { return @{} } - -$ArtifactBasePath = "$RepoRoot\obj\_artifacts" -$ArtifactPath = "$ArtifactBasePath\VSInsertion" -if (-not (Test-Path $ArtifactPath)) { New-Item -ItemType Directory -Path $ArtifactPath | Out-Null } - -$profilingInputs = [xml](Get-Content -Path "$PSScriptRoot\..\ProfilingInputs.props") -$profilingInputs.Project.ItemGroup.TestStore.Include = "vstsdrop:" + (& "$PSScriptRoot\..\variables\ProfilingInputsDropName.ps1") -$profilingInputs.Save("$ArtifactPath\ProfilingInputs.props") - -$nbgv = & "$PSScriptRoot\..\Get-nbgv.ps1" -$InsertionMetadataVersion = $(& $nbgv get-version -p "$RepoRoot\src" -f json | ConvertFrom-Json).NuGetPackageVersion -if ($env:BUILD_BUILDID) { - # We must ensure unique versions for the insertion metadata package so - # it can contain information that is unique to this build. - # In particular it includes the ProfilingInputsDropName, which contains the BuildId. - # A non-unique package version here may collide with a prior run of this same commit, - # ultimately resulting in a failure of the optprof run. - $InsertionMetadataVersion += '.' + $env:BUILD_BUILDID -} -& (& "$PSScriptRoot\..\Get-NuGetTool.ps1") pack "$PSScriptRoot\..\InsertionMetadataPackage.nuspec" -OutputDirectory $CoreXTPackages -BasePath $ArtifactPath -Version $InsertionMetadataVersion | Out-Null -if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE -} - -@{ - "$NuGetPackages" = (Get-ChildItem -Recurse $NuGetPackages); - "$CoreXTPackages" = (Get-ChildItem "$CoreXTPackages\Microsoft.VisualStudio.Threading.VSInsertionMetadata.$InsertionMetadataVersion.nupkg"); -} diff --git a/azure-pipelines/artifacts/_pipelines.ps1 b/azure-pipelines/artifacts/_pipelines.ps1 deleted file mode 100644 index 73a3af0ac..000000000 --- a/azure-pipelines/artifacts/_pipelines.ps1 +++ /dev/null @@ -1,15 +0,0 @@ -# This script translates all the artifacts described by _all.ps1 -# into commands that instruct Azure Pipelines to actually collect those artifacts. - -param ( - [string]$ArtifactNameSuffix -) - -& "$PSScriptRoot/_stage_all.ps1" -ArtifactNameSuffix $ArtifactNameSuffix |% { - Write-Host "##vso[artifact.upload containerfolder=$($_.Name);artifactname=$($_.Name);]$($_.Path)" - - # Set a variable which will out-live this script so that a subsequent attempt to collect and upload artifacts - # will skip this one from a check in the _all.ps1 script. - $varName = "ARTIFACTUPLOADED_$($_.Name.ToUpper())" - Write-Host "##vso[task.setvariable variable=$varName]true" -} diff --git a/azure-pipelines/artifacts/_stage_all.ps1 b/azure-pipelines/artifacts/_stage_all.ps1 deleted file mode 100644 index 4788a3f5b..000000000 --- a/azure-pipelines/artifacts/_stage_all.ps1 +++ /dev/null @@ -1,59 +0,0 @@ -# This script links all the artifacts described by _all.ps1 -# into a staging directory, reading for uploading to a cloud build artifact store. -# It returns a sequence of objects with Name and Path properties. - -param ( - [string]$ArtifactNameSuffix -) - -$RepoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot (Join-Path .. ..))) -if ($env:BUILD_ARTIFACTSTAGINGDIRECTORY) { - $ArtifactStagingFolder = $env:BUILD_ARTIFACTSTAGINGDIRECTORY -} else { - $ArtifactStagingFolder = Join-Path $RepoRoot (Join-Path obj _artifacts) - if (Test-Path $ArtifactStagingFolder) { - Remove-Item $ArtifactStagingFolder -Recurse -Force - } -} - -function Create-SymbolicLink { - param ( - $Link, - $Target - ) - - if ($Link -eq $Target) { - return - } - - if (Test-Path $Link) { Remove-Item $Link } - $LinkContainer = Split-Path $Link -Parent - if (!(Test-Path $LinkContainer)) { mkdir $LinkContainer } - Write-Verbose "Linking $Link to $Target" - if ($IsMacOS -or $IsLinux) { - ln $Target $Link | Out-Null - } else { - cmd /c "mklink `"$Link`" `"$Target`"" | Out-Null - } -} - -# Stage all artifacts -$Artifacts = & "$PSScriptRoot\_all.ps1" -ArtifactNameSuffix $ArtifactNameSuffix -$Artifacts |% { - $DestinationFolder = (Join-Path (Join-Path $ArtifactStagingFolder "$($_.ArtifactName)$ArtifactNameSuffix") $_.ContainerFolder).TrimEnd('\') - $Name = "$(Split-Path $_.Source -Leaf)" - - #Write-Host "$($_.Source) -> $($_.ArtifactName)\$($_.ContainerFolder)" -ForegroundColor Yellow - - if (-not (Test-Path $DestinationFolder)) { New-Item -ItemType Directory -Path $DestinationFolder | Out-Null } - if (Test-Path -PathType Leaf $_.Source) { # skip folders - Create-SymbolicLink -Link (Join-Path $DestinationFolder $Name) -Target $_.Source - } -} - -$Artifacts |% { "$($_.ArtifactName)$ArtifactNameSuffix" } | Get-Unique |% { - $artifact = New-Object -TypeName PSObject - Add-Member -InputObject $artifact -MemberType NoteProperty -Name Name -Value $_ - Add-Member -InputObject $artifact -MemberType NoteProperty -Name Path -Value (Join-Path $ArtifactStagingFolder $_) - Write-Output $artifact -} diff --git a/azure-pipelines/artifacts/build_logs.ps1 b/azure-pipelines/artifacts/build_logs.ps1 deleted file mode 100644 index b55ba48f3..000000000 --- a/azure-pipelines/artifacts/build_logs.ps1 +++ /dev/null @@ -1,12 +0,0 @@ -if ($env:BUILD_ARTIFACTSTAGINGDIRECTORY) { - $artifactsRoot = $env:BUILD_ARTIFACTSTAGINGDIRECTORY -} else { - $RepoRoot = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..") - $artifactsRoot = "$RepoRoot\bin" -} - -if (!(Test-Path $artifactsRoot/build_logs)) { return } - -@{ - "$artifactsRoot/build_logs" = (Get-ChildItem -Recurse "$artifactsRoot/build_logs") -} diff --git a/azure-pipelines/artifacts/coverageResults.ps1 b/azure-pipelines/artifacts/coverageResults.ps1 deleted file mode 100644 index 8fdb3f720..000000000 --- a/azure-pipelines/artifacts/coverageResults.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -$RepoRoot = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..") - -# Prepare code coverage reports for merging on another machine -if ($env:SYSTEM_DEFAULTWORKINGDIRECTORY) { - Write-Host "Substituting $env:SYSTEM_DEFAULTWORKINGDIRECTORY with `"{reporoot}`"" - $reports = Get-ChildItem "$RepoRoot/bin/coverage.*cobertura.xml" -Recurse - $reports |% { - $content = Get-Content -Path $_ |% { $_ -Replace [regex]::Escape($env:SYSTEM_DEFAULTWORKINGDIRECTORY), "{reporoot}" } - Set-Content -Path $_ -Value $content -Encoding UTF8 - } -} else { - Write-Warning "coverageResults: Azure Pipelines not detected. Machine-neutral token replacement skipped." -} - -if (!((Test-Path $RepoRoot\bin) -and (Test-Path $RepoRoot\obj))) { return } - -@{ - $RepoRoot = ( - @(Get-ChildItem "$RepoRoot\bin\coverage.*cobertura.xml" -Recurse) + - (Get-ChildItem "$RepoRoot\obj\*.cs" -Recurse) - ); -} diff --git a/azure-pipelines/artifacts/testResults.ps1 b/azure-pipelines/artifacts/testResults.ps1 deleted file mode 100644 index 862155da7..000000000 --- a/azure-pipelines/artifacts/testResults.ps1 +++ /dev/null @@ -1,18 +0,0 @@ -$result = @{} - -if ($env:AGENT_TEMPDIRECTORY) { - # The DotNetCoreCLI uses an alternate location to publish these files - $guidRegex = '^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$' - $result[$env:AGENT_TEMPDIRECTORY] = (Get-ChildItem $env:AGENT_TEMPDIRECTORY -Directory |? { $_.Name -match $guidRegex } |% { Get-ChildItem "$($_.FullName)\dotnet*.dmp","$($_.FullName)\testhost*.dmp","$($_.FullName)\Sequence_*.xml" -Recurse }); -} -else { - $testRoot = Resolve-Path "$PSScriptRoot\..\..\test" - $result[$testRoot] = (Get-ChildItem "$testRoot\TestResults" -Recurse -Directory | Get-ChildItem -Recurse -File) -} - -$testlogsPath = "$env:BUILD_ARTIFACTSTAGINGDIRECTORY\test_logs" -if (Test-Path $testlogsPath) { - $result[$testlogsPath] = Get-ChildItem "$testlogsPath\*"; -} - -$result diff --git a/azure-pipelines/artifacts/test_symbols.ps1 b/azure-pipelines/artifacts/test_symbols.ps1 deleted file mode 100644 index d65ad0ae5..000000000 --- a/azure-pipelines/artifacts/test_symbols.ps1 +++ /dev/null @@ -1,12 +0,0 @@ -# This doesn't work off Windows, nor do we need to convert symbols on multiple OS agents -if ($IsMacOS -or $IsLinux) { - return; -} - -$BinPath = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..\bin") -if (!(Test-Path $BinPath)) { return } -$symbolfiles = & "$PSScriptRoot\..\Get-SymbolFiles.ps1" -Path $BinPath -Tests | Get-Unique - -@{ - "$BinPath" = $SymbolFiles; -} diff --git a/azure-pipelines/build.yml b/azure-pipelines/build.yml index 4f84e66bd..646659295 100644 --- a/azure-pipelines/build.yml +++ b/azure-pipelines/build.yml @@ -1,84 +1,367 @@ parameters: -- name: windowsPool +##### The following parameters are not set by other YAML files that import this one, +##### but we use parameters because they support rich types and defaults. +##### Feel free to adjust their default value as needed. + +# Whether this repo uses OptProf to optimize the built binaries. +# When enabling this, be sure to update these files: +# - OptProf.targets: InstallationPath and match TestCase selection with what's in the VS repo. +# - The project file(s) for the libraries to optimize must import OptProf.targets (for multi-targeted projects, only import it for ONE target). +# - OptProf.yml: Search for LibraryName (or your library's name) and verify that those names are appropriate. +# - OptProf_part2.yml: Search for LibraryName (or your library's name) and verify that those names are appropriate. +# and create pipelines for OptProf.yml, OptProf_part2.yml +- name: EnableOptProf + type: boolean + default: true +# Whether this repo is localized. +- name: EnableLocalization + type: boolean + default: true +# Whether to run `dotnet format` as part of the build to ensure code style consistency. +# This is just one of a a few mechanisms to enforce code style consistency. +- name: EnableDotNetFormatCheck + type: boolean + default: false # enable when we get it to pass +# This lists the names of the artifacts that will be published *from every OS build agent*. +# Any new tools/artifacts/*.ps1 script needs to be added to this list. +# If an artifact is only generated or collected on one OS, it should NOT be listed here, +# but should be manually added to the `outputs:` field in the appropriate OS job. +- name: artifact_names type: object default: - vmImage: windows-2022 -- name: ShouldSkipOptimize -- name: includeMacOS + - name: build_logs + - name: coverageResults + - name: deployables + sbomEnabled: true + - name: projectAssetsJson + - name: symbols + - name: testResults + testOnly: true + - name: test_symbols + testOnly: true + - name: Variables +# The Enable*Build parameters turn non-Windows agents on or off. +# Their default value should be based on whether the build and tests are expected/required to pass on that platform. +# Callers (e.g. Official.yml) *may* expose these parameters at queue-time in order to turn OFF optional agents. +- name: EnableLinuxBuild + type: boolean + default: true +- name: EnableMacOSBuild + type: boolean + default: true + +##### 👆🏼 You MAY change the defaults above. +##### 👇🏼 You should NOT change the defaults below. + +##### The following parameters are expected to be set by other YAML files that import this one. +##### Those without defaults require explicit values to be provided by our importers. + +# Indicates whether the entrypoint file is 1ESPT compliant. Use this parameter to switch between publish tasks to fit 1ES or non-1ES needs. +- name: Is1ESPT + type: boolean + +# Indicates whether the 'official' 1ES PT templates are being used (as opposed to the unofficial ones). +- name: Is1ESPTOfficial + type: boolean + default: false + +- name: RealSign + type: boolean + default: false + +- name: RunTests + type: boolean + default: true + +- name: PublishCodeCoverage + type: boolean + default: true + +# Whether this is a special one-off build for inserting into VS for a validation insertion PR (that will never be merged). +- name: SkipCodesignVerify + type: boolean + default: false + - name: EnableAPIScan type: boolean default: false +# This parameter exists to provide a workaround to get a build out even when no OptProf profiling outputs can be found. +# Entrypoint yaml files like official.yml should expose this as a queue-time setting when EnableOptProf is true in this file. +# The OptProf.yml entrypoint sets this parameter to true so that collecting profile data isn't blocked by a prior lack of profile data. +- name: ShouldSkipOptimize + type: boolean + default: false + +# The pool parameters are set to defaults that work in the azure-public AzDO account. +# They are overridden by callers for the devdiv AzDO account to use 1ES compliant pools. +- name: windowsPool + type: object + default: + vmImage: windows-2025 +- name: linuxPool + type: object + default: + vmImage: ubuntu-24.04 +- name: macOSPool + type: object + default: + vmImage: macOS-15 + jobs: - job: Windows pool: ${{ parameters.windowsPool }} - variables: - - ${{ if eq(variables['system.collectionId'], '011b8bdf-6d56-4f87-be0d-0092136884d9') }}: - # https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/25351/APIScan-step-by-step-guide-to-setting-up-a-Pipeline - - group: VSCloudServices-APIScan - steps: - - checkout: self - clean: true + timeoutInMinutes: 180 # Give plenty of time due to real signing + ${{ if eq(variables['system.collectionId'], '011b8bdf-6d56-4f87-be0d-0092136884d9') }}: + templateContext: + mb: + signing: + enabled: true + zipSources: false + ${{ if parameters.RealSign }}: + signType: real + signWithProd: true + ${{ else }}: + signType: test + sbom: + enabled: true + sbomToolVersion: 5.0.3 + localization: + enabled: ${{ parameters.EnableLocalization }} + ${{ if eq(variables['Build.Reason'], 'pullRequest') }}: + languages: ENU,JPN + optprof: + enabled: ${{ parameters.EnableOptProf }} + ProfilingInputsDropName: $(ProfilingInputsDropName) + GeneratePropsFile: true + PropsPath: $(Build.ArtifactStagingDirectory)/InsertionOutputs/$(ProfilingInputsPropsName) + OptimizationInputsLookupMethod: GitTagRepo + GitTagProject: DevDiv + GitTagRepo: VS + ShouldSkipOptimize: ${{ parameters.ShouldSkipOptimize }} + AccessToken: $(System.AccessToken) + mbpresteps: + - checkout: self + fetchDepth: 0 # avoid shallow clone so nbgv can do its work. + clean: true + - ${{ if parameters.EnableOptProf }}: + - powershell: | + Write-Host "##vso[task.setvariable variable=PROFILINGINPUTSDROPNAME]$(tools/variables/ProfilingInputsDropName.ps1)" + Write-Host "##vso[task.setvariable variable=PROFILINGINPUTSPROPSNAME]$(tools/variables/ProfilingInputsPropsName.ps1)" + displayName: ⚙ Setting variables for optprof + sdl: + binskim: + analyzeTargetGlob: $(Build.ArtifactStagingDirectory)\symbols-Windows\** - - ${{ if eq(variables['Build.Reason'], 'Schedule') }}: - - template: schedule-only-steps.yml + outputParentDirectory: $(Build.ArtifactStagingDirectory) + outputs: + - ${{ each artifact in parameters.artifact_names }}: + - ${{ if or(ne(artifact.testOnly, 'true'), parameters.RunTests) }}: + - output: pipelineArtifact + displayName: 📢 Publish ${{ artifact.name }}-Windows + targetPath: $(Build.ArtifactStagingDirectory)/${{ artifact.name }}-Windows + artifactName: ${{ artifact.name }}-Windows + ${{ if and(parameters.Is1ESPTOfficial, eq(artifact.sbomEnabled, 'true')) }}: + sbomEnabled: true + - output: pipelineArtifact + displayName: 📢 Publish ${{ artifact.name }}-Windows (for failed attempts) + targetPath: $(Build.ArtifactStagingDirectory)/${{ artifact.name }}-Windows + artifactName: ${{ artifact.name }}-Windows-$(System.PhaseAttempt) + ${{ if and(parameters.Is1ESPTOfficial, eq(artifact.sbomEnabled, 'true')) }}: + sbomEnabled: true + condition: failed() + - output: pipelineArtifact + displayName: 📢 Publish VSInsertion-Windows + targetPath: $(Build.ArtifactStagingDirectory)/VSInsertion-Windows + artifactName: VSInsertion-Windows + - ${{ if parameters.EnableLocalization }}: + - output: pipelineArtifact + displayName: 📢 Publish LocBin-Windows + targetPath: $(Build.ArtifactStagingDirectory)/LocBin-Windows + artifactName: LocBin-Windows + - ${{ if parameters.EnableAPIScan }}: + - output: pipelineArtifact + displayName: 📢 Publish APIScanInputs + targetPath: $(Build.ArtifactStagingDirectory)/APIScanInputs-Windows + artifactName: APIScanInputs + - ${{ if parameters.EnableOptProf }}: + - output: artifactsDrop + displayName: 📢 Publish to Artifact Services - ProfilingInputs + dropServiceURI: https://devdiv.artifacts.visualstudio.com + buildNumber: $(ProfilingInputsDropName) + sourcePath: $(Build.ArtifactStagingDirectory)\OptProf\ProfilingInputs + toLowerCase: false + retentionDays: 500 + condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) + steps: + - ${{ if not(parameters.Is1ESPT) }}: + - checkout: self + fetchDepth: 0 # avoid shallow clone so nbgv can do its work. + clean: true + - ${{ if parameters.EnableOptProf }}: + - powershell: Write-Host "##vso[task.setvariable variable=PROFILINGINPUTSDROPNAME]$(tools/variables/ProfilingInputsDropName.ps1)" + displayName: ⚙ Set ProfilingInputsDropName for optprof - template: install-dependencies.yml - - powershell: '& (./azure-pipelines/Get-nbgv.ps1) cloud -ca' - displayName: Set build number + - script: dotnet nbgv cloud -ca + displayName: ⚙ Set build number + name: nbgv - ${{ if eq(variables['system.collectionId'], '011b8bdf-6d56-4f87-be0d-0092136884d9') }}: - template: microbuild.before.yml parameters: + EnableLocalization: ${{ parameters.EnableLocalization }} ShouldSkipOptimize: ${{ parameters.ShouldSkipOptimize }} + RealSign: ${{ parameters.RealSign }} - template: dotnet.yml + parameters: + Is1ESPT: ${{ parameters.Is1ESPT }} + RunTests: ${{ parameters.RunTests }} + osRID: win + + - ${{ if and(parameters.EnableDotNetFormatCheck, not(parameters.EnableLinuxBuild)) }}: + - script: dotnet format --verify-no-changes + displayName: 💅 Verify formatted code + env: + dotnetformat: true # part of a workaround for https://github.com/dotnet/sdk/issues/44951 - ${{ if eq(variables['system.collectionId'], '011b8bdf-6d56-4f87-be0d-0092136884d9') }}: - template: microbuild.after.yml parameters: - EnableAPIScan: ${{ parameters.EnableAPIScan }} - # Repeat this step to scoop up any artifacts that would only be collected after running microbuild.after.yml - - powershell: azure-pipelines/artifacts/_pipelines.ps1 -ArtifactNameSuffix "-$(Agent.JobName)" - failOnStderr: true - displayName: Publish artifacts - condition: succeededOrFailed() - -- job: Linux - condition: ne(variables['OptProf'], 'true') - pool: - vmImage: Ubuntu 20.04 - steps: - - checkout: self - clean: true - - template: install-dependencies.yml - - template: dotnet.yml + SkipCodesignVerify: ${{ parameters.SkipCodesignVerify }} -- job: macOS - condition: and(${{ parameters.includeMacOS }}, ne(variables['OptProf'], 'true')) - pool: - vmImage: macOS-10.15 - steps: - - checkout: self - clean: true - - template: install-dependencies.yml - - template: dotnet.yml +- ${{ if parameters.EnableLinuxBuild }}: + - job: Linux + pool: ${{ parameters.linuxPool }} + ${{ if eq(variables['system.collectionId'], '011b8bdf-6d56-4f87-be0d-0092136884d9') }}: + templateContext: + mb: + ${{ if parameters.RealSign }}: + signing: + enabled: false # enable when building unique artifacts on this agent that must be signed + signType: real + signWithProd: true + outputParentDirectory: $(Build.ArtifactStagingDirectory) + outputs: + - ${{ each artifact in parameters.artifact_names }}: + - ${{ if or(ne(artifact.testOnly, 'true'), parameters.RunTests) }}: + - output: pipelineArtifact + displayName: 📢 Publish ${{ artifact.name }}-Linux + targetPath: $(Build.ArtifactStagingDirectory)/${{ artifact.name }}-Linux + artifactName: ${{ artifact.name }}-Linux + ${{ if and(parameters.Is1ESPTOfficial, eq(artifact.sbomEnabled, 'true')) }}: + sbomEnabled: true + - output: pipelineArtifact + displayName: 📢 Publish ${{ artifact.name }}-Linux (for failed attempts) + targetPath: $(Build.ArtifactStagingDirectory)/${{ artifact.name }}-Linux + artifactName: ${{ artifact.name }}-Linux-$(System.PhaseAttempt) + ${{ if and(parameters.Is1ESPTOfficial, eq(artifact.sbomEnabled, 'true')) }}: + sbomEnabled: true + condition: failed() + steps: + - checkout: self + fetchDepth: 0 # avoid shallow clone so nbgv can do its work. + clean: true + - template: install-dependencies.yml + - template: dotnet.yml + parameters: + Is1ESPT: ${{ parameters.Is1ESPT }} + RunTests: ${{ parameters.RunTests }} + BuildRequiresAccessToken: ${{ parameters.RealSign }} # Real signing on non-Windows machines requires passing through access token to build steps that sign + osRID: linux + - ${{ if parameters.EnableDotNetFormatCheck }}: + - script: dotnet format --verify-no-changes + displayName: 💅 Verify formatted code + env: + dotnetformat: true # part of a workaround for https://github.com/dotnet/sdk/issues/44951 + +- ${{ if parameters.EnableMacOSBuild }}: + - job: macOS + pool: ${{ parameters.macOSPool }} + ${{ if eq(variables['system.collectionId'], '011b8bdf-6d56-4f87-be0d-0092136884d9') }}: + templateContext: + mb: + ${{ if parameters.RealSign }}: + signing: + enabled: false # enable when building unique artifacts on this agent that must be signed + signType: real + signWithProd: true + outputParentDirectory: $(Build.ArtifactStagingDirectory) + outputs: + - ${{ each artifact in parameters.artifact_names }}: + - ${{ if or(ne(artifact.testOnly, 'true'), parameters.RunTests) }}: + - output: pipelineArtifact + displayName: 📢 Publish ${{ artifact.name }}-macOS + targetPath: $(Build.ArtifactStagingDirectory)/${{ artifact.name }}-macOS + artifactName: ${{ artifact.name }}-macOS + ${{ if and(parameters.Is1ESPTOfficial, eq(artifact.sbomEnabled, 'true')) }}: + sbomEnabled: true + - output: pipelineArtifact + displayName: 📢 Publish ${{ artifact.name }}-macOS (for failed attempts) + targetPath: $(Build.ArtifactStagingDirectory)/${{ artifact.name }}-macOS + artifactName: ${{ artifact.name }}-macOS-$(System.PhaseAttempt) + ${{ if and(parameters.Is1ESPTOfficial, eq(artifact.sbomEnabled, 'true')) }}: + sbomEnabled: true + condition: failed() + steps: + - checkout: self + fetchDepth: 0 # avoid shallow clone so nbgv can do its work. + clean: true + - template: install-dependencies.yml + - template: dotnet.yml + parameters: + Is1ESPT: ${{ parameters.Is1ESPT }} + RunTests: ${{ parameters.RunTests }} + BuildRequiresAccessToken: ${{ parameters.RealSign }} # Real signing on non-Windows machines requires passing through access token to build steps that sign + osRID: osx - job: WrapUp dependsOn: - Windows - - Linux - - macOS - pool: - vmImage: Ubuntu 20.04 - condition: ne(variables['OptProf'], 'true') + - ${{ if parameters.EnableLinuxBuild }}: + - Linux + - ${{ if parameters.EnableMacOSBuild }}: + - macOS + pool: ${{ parameters.windowsPool }} # Use Windows agent because PublishSymbols task requires it (https://github.com/microsoft/azure-pipelines-tasks/issues/13821). + condition: succeededOrFailed() + variables: + ONEES_ENFORCED_CODEQL_ENABLED: false # CodeQL runs on build jobs, we don't need it here + ${{ if eq(variables['system.collectionId'], '011b8bdf-6d56-4f87-be0d-0092136884d9') }}: + templateContext: + ${{ if not(parameters.RealSign) }}: + mb: + signing: # if the build is test-signed, install the signing plugin so that CSVTestSignPolicy.xml is available + enabled: true + zipSources: false + signType: test + outputParentDirectory: $(Build.ArtifactStagingDirectory) + outputs: + - output: pipelineArtifact + displayName: 📢 Publish symbols-legacy + targetPath: $(Build.ArtifactStagingDirectory)/symbols-legacy + artifactName: symbols-legacy + condition: succeededOrFailed() steps: - checkout: self + fetchDepth: 0 # avoid shallow clone so nbgv can do its work. clean: true - template: install-dependencies.yml parameters: initArgs: -NoRestore - - template: publish-codecoverage.yml + - template: publish-symbols.yml + parameters: + EnableLinuxBuild: ${{ parameters.EnableLinuxBuild }} + EnableMacOSBuild: ${{ parameters.EnableMacOSBuild }} + - ${{ if and(parameters.RunTests, parameters.PublishCodeCoverage) }}: + - template: publish-codecoverage.yml + parameters: + EnableLinuxBuild: ${{ parameters.EnableLinuxBuild }} + EnableMacOSBuild: ${{ parameters.EnableMacOSBuild }} + +- ${{ if parameters.EnableAPIScan }}: + - template: apiscan.yml parameters: - includeMacOS: ${{ parameters.includeMacOS }} + windowsPool: ${{ parameters.windowsPool }} + RealSign: ${{ parameters.RealSign }} diff --git a/azure-pipelines/dotnet.yml b/azure-pipelines/dotnet.yml index 527b43b85..fb6a8c3d3 100644 --- a/azure-pipelines/dotnet.yml +++ b/azure-pipelines/dotnet.yml @@ -1,78 +1,49 @@ -steps: -# We use VSBuild instead of "dotnet build" on Windows because dllexport doesn't work on dotnet build. -- task: VSBuild@1 - displayName: Build Visual Studio solution - inputs: - msbuildArgs: /t:build,pack /m /bl:"$(Build.ArtifactStagingDirectory)/build_logs/msbuild.binlog" - platform: Any CPU - configuration: $(BuildConfiguration) - condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) - -- script: dotnet build /t:build,pack --no-restore -c $(BuildConfiguration) /p:Platform=NonWindows /v:m /bl:"$(Build.ArtifactStagingDirectory)/build_logs/build.binlog" - displayName: dotnet build & pack - condition: and(succeeded(), ne(variables['Agent.OS'], 'Windows_NT')) - -- task: DotNetCoreCLI@2 - displayName: dotnet test -f net472 - inputs: - command: test - arguments: --no-build -c $(BuildConfiguration) -f net472 --filter "TestCategory!=FailsInCloudTest" -v n /p:CollectCoverage=true --settings "$(Build.Repository.LocalPath)/azure-pipelines/$(Agent.OS).runsettings" /bl:"$(Build.ArtifactStagingDirectory)/build_logs/test_net472.binlog" --diag "$(Build.ArtifactStagingDirectory)/test_logs/net472.txt" - testRunTitle: net472-$(Agent.JobName) - condition: and(ne(variables['OptProf'], 'true'), eq(variables['Agent.OS'], 'Windows_NT')) +parameters: +- name: RunTests +- name: Is1ESPT + type: boolean +- name: BuildRequiresAccessToken + type: boolean + default: false +- name: osRID + type: string -- task: DotNetCoreCLI@2 - displayName: dotnet test -f netcoreapp3.1 - inputs: - command: test - arguments: --no-build -c $(BuildConfiguration) -f netcoreapp3.1 --filter "TestCategory!=FailsInCloudTest" -v n /p:CollectCoverage=true --settings "$(Build.Repository.LocalPath)/azure-pipelines/$(Agent.OS).runsettings" /bl:"$(Build.ArtifactStagingDirectory)/build_logs/test_netcoreapp3.1.binlog" --diag "$(Build.ArtifactStagingDirectory)/test_logs/netcoreapp3.1.txt" - testRunTitle: netcoreapp3.1-$(Agent.JobName) - workingDirectory: test/Microsoft.VisualStudio.Threading.Tests - condition: ne(variables['OptProf'], 'true') +steps: -- task: DotNetCoreCLI@2 - displayName: dotnet test -f net5.0 - inputs: - command: test - arguments: --no-build -c $(BuildConfiguration) -f net5.0 --filter "TestCategory!=FailsInCloudTest" -v n /p:CollectCoverage=true --settings "$(Build.Repository.LocalPath)/azure-pipelines/$(Agent.OS).runsettings" /bl:"$(Build.ArtifactStagingDirectory)/build_logs/test_net5.0.binlog" --diag "$(Build.ArtifactStagingDirectory)/test_logs/net5.0.txt" - testRunTitle: net5.0-$(Agent.JobName) - workingDirectory: test/Microsoft.VisualStudio.Threading.Tests - condition: ne(variables['OptProf'], 'true') +- script: dotnet build tools/dirs.proj -t:build,pack,publish --no-restore -c $(BuildConfiguration) -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904,LOCTASK002 /bl:"$(Build.ArtifactStagingDirectory)/build_logs/build.binlog" + displayName: 🛠 dotnet build + ${{ if parameters.BuildRequiresAccessToken }}: + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) -# We have to artifically run this script so that the extra .nupkg is produced for variables/InsertConfigValues.ps1 to notice. -- powershell: azure-pipelines\artifacts\VSInsertion.ps1 - displayName: Prepare VSInsertion artifact - condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) +- powershell: tools/dotnet-test-cloud.ps1 -Configuration $(BuildConfiguration) -Agent $(Agent.JobName) -PublishResults + displayName: 🧪 dotnet test + condition: and(succeeded(), ${{ parameters.RunTests }}) -- powershell: azure-pipelines/variables/_pipelines.ps1 +- powershell: tools/variables/_define.ps1 failOnStderr: true - displayName: Update pipeline variables based on build outputs + displayName: ⚙ Update pipeline variables based on build outputs condition: succeededOrFailed() -- powershell: azure-pipelines/artifacts/_pipelines.ps1 -ArtifactNameSuffix "-$(Agent.JobName)" - failOnStderr: true - displayName: Publish artifacts - condition: succeededOrFailed() - -- task: PublishSymbols@2 - inputs: - SymbolsFolder: $(Build.ArtifactStagingDirectory)/symbols-$(Agent.JobName) - SearchPattern: '**/*.pdb' - IndexSources: false - SymbolServerType: TeamServices - displayName: Publish symbols to symbol server - condition: eq(variables['Agent.OS'], 'Windows_NT') # Execute on failed test runs too. Windows-only till https://github.com/microsoft/azure-pipelines-tasks/issues/13821 is fixed. - -- task: PublishSymbols@2 - inputs: - SymbolsFolder: $(Build.ArtifactStagingDirectory)/test_symbols-$(Agent.JobName) - SearchPattern: '**/*.pdb' - IndexSources: false - SymbolServerType: TeamServices - displayName: Publish test symbols to symbol server - condition: and(failed(), eq(variables['Agent.OS'], 'Windows_NT')) # Execute on failed test runs only. - -- bash: bash <(curl -s https://codecov.io/bash) - displayName: Publish code coverage results to codecov.io - condition: ne(variables['codecov_token'], '') - timeoutInMinutes: 3 - continueOnError: true +- ${{ if parameters.Is1ESPT }}: + - powershell: azure-pipelines/publish_artifacts.ps1 -StageOnly -AvoidSymbolicLinks -ArtifactNameSuffix "-$(Agent.JobName)" -Verbose + failOnStderr: true + displayName: 📢 Stage artifacts + condition: succeededOrFailed() +- ${{ else }}: + - powershell: azure-pipelines/publish_artifacts.ps1 -ArtifactNameSuffix "-$(Agent.JobName)" -Verbose + failOnStderr: true + displayName: 📢 Publish artifacts + condition: succeededOrFailed() + +- ${{ if parameters.RunTests }}: + - powershell: | + $ArtifactStagingFolder = & "tools/Get-ArtifactsStagingDirectory.ps1" + $CoverageResultsFolder = Join-Path $ArtifactStagingFolder "coverageResults-$(Agent.JobName)" + tools/publish-CodeCov.ps1 -CodeCovToken "$(CODECOV_TOKEN)" -PathToCodeCoverage "$CoverageResultsFolder" -Name "$(Agent.JobName) Coverage Results" -Flags "$(Agent.JobName)" + displayName: 📢 Publish code coverage results to codecov.io + timeoutInMinutes: 3 + continueOnError: true + # Set the CODECOV_TOKEN variable in your Azure Pipeline to enable code coverage reporting + # Get a token from https://codecov.io/ + condition: and(succeeded(), ne(variables['CODECOV_TOKEN'], '')) diff --git a/azure-pipelines/install-dependencies.yml b/azure-pipelines/install-dependencies.yml index 4f848b099..76b823901 100644 --- a/azure-pipelines/install-dependencies.yml +++ b/azure-pipelines/install-dependencies.yml @@ -1,12 +1,26 @@ parameters: - initArgs: +- name: initArgs + type: string + default: '' +- name: needsAzurePublicFeeds + type: boolean + default: true # If nuget.config pulls from the azure-public account, we need to authenticate when building on the devdiv account. +- name: setVariables + type: boolean + default: true steps: +- ${{ if and(parameters.needsAzurePublicFeeds, eq(variables['system.collectionId'], '011b8bdf-6d56-4f87-be0d-0092136884d9')) }}: + - template: WIFtoPATauth.yml + parameters: + wifServiceConnectionName: azure-public/vside package pull + deadPATServiceConnectionId: 46f0d4d4-9fff-4c58-a1ab-3b8f97e3b78a # azure-public/msft_consumption_public -- task: NuGetAuthenticate@0 - displayName: Authenticate NuGet feeds +- task: NuGetAuthenticate@1 + displayName: 🔏 Authenticate NuGet feeds inputs: - forceReinstallCredentialProvider: true + ${{ if and(parameters.needsAzurePublicFeeds, eq(variables['system.collectionId'], '011b8bdf-6d56-4f87-be0d-0092136884d9')) }}: + nuGetServiceConnections: azure-public/msft_consumption_public - powershell: | $AccessToken = '$(System.AccessToken)' # Avoid specifying the access token directly on the init.ps1 command line to avoid it showing up in errors @@ -17,9 +31,10 @@ steps: if (Get-Command mono -ErrorAction SilentlyContinue) { mono --version } - displayName: Install prerequisites + displayName: ⚙ Install prerequisites -- powershell: azure-pipelines/variables/_pipelines.ps1 - failOnStderr: true - displayName: Set pipeline variables based on source - name: SetPipelineVariables +- ${{ if parameters.setVariables }}: + - powershell: tools/variables/_define.ps1 + failOnStderr: true + displayName: ⚙ Set pipeline variables based on source + name: SetPipelineVariables diff --git a/azure-pipelines/justnugetorg.nuget.config b/azure-pipelines/justnugetorg.nuget.config deleted file mode 100644 index 765346e53..000000000 --- a/azure-pipelines/justnugetorg.nuget.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/azure-pipelines/libtemplate-update.yml b/azure-pipelines/libtemplate-update.yml new file mode 100644 index 000000000..acbfbacef --- /dev/null +++ b/azure-pipelines/libtemplate-update.yml @@ -0,0 +1,172 @@ +# This pipeline schedules regular merges of Library.Template into a repo that is based on it. +# Only Azure Repos are supported. GitHub support comes via a GitHub Actions workflow. + +trigger: none +pr: none +schedules: +- cron: "0 3 * * Mon" # Sun @ 8 or 9 PM Mountain Time (depending on DST) + displayName: Weekly trigger + branches: + include: + - main + always: true + +resources: + repositories: + - repository: MicroBuildTemplate + type: git + name: 1ESPipelineTemplates/MicroBuildTemplate + ref: refs/tags/release + +parameters: +- name: AutoComplete + displayName: Auto-complete pull request + type: boolean + default: false + +variables: +- template: GlobalVariables.yml + +extends: + template: azure-pipelines/MicroBuild.1ES.Unofficial.yml@MicroBuildTemplate + parameters: + settings: + networkIsolationPolicy: Permissive,CFSClean2 + sdl: + sourceAnalysisPool: + name: AzurePipelines-EO + demands: + - ImageOverride -equals 1ESPT-Windows2022 + credscan: + enabled: false + + stages: + - stage: Merge + jobs: + - job: merge + pool: + name: AzurePipelines-EO + demands: + - ImageOverride -equals 1ESPT-Ubuntu24.04 + os: Linux + steps: + - checkout: self + fetchDepth: 0 + clean: true + - pwsh: | + $LibTemplateBranch = & ./tools/Get-LibTemplateBasis.ps1 -ErrorIfNotRelated + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + + git fetch https://github.com/aarnott/Library.Template $LibTemplateBranch + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + $LibTemplateCommit = git rev-parse FETCH_HEAD + + if ((git rev-list FETCH_HEAD ^HEAD --count) -eq 0) { + Write-Host "There are no Library.Template updates to merge." + exit 0 + } + + $UpdateBranchName = 'auto/libtemplateUpdate' + git -c http.extraheader="AUTHORIZATION: bearer $(System.AccessToken)" push origin -f FETCH_HEAD:refs/heads/$UpdateBranchName + + Write-Host "Creating pull request" + $contentType = 'application/json'; + $headers = @{ Authorization = 'Bearer $(System.AccessToken)' }; + $rawRequest = @{ + sourceRefName = "refs/heads/$UpdateBranchName"; + targetRefName = "refs/heads/main"; + title = 'Merge latest Library.Template'; + description = "This merges the latest features and fixes from [Library.Template's $LibTemplateBranch branch](https://github.com/AArnott/Library.Template/tree/$LibTemplateBranch)."; + } + $request = ConvertTo-Json $rawRequest + + $prApiBaseUri = '$(System.TeamFoundationCollectionUri)/$(System.TeamProject)/_apis/git/repositories/$(Build.Repository.ID)/pullrequests' + $prCreationUri = $prApiBaseUri + "?api-version=6.0" + Write-Host "POST $prCreationUri" + Write-Host $request + + $prCreationResult = Invoke-RestMethod -uri $prCreationUri -method POST -Headers $headers -ContentType $contentType -Body $request + $prUrl = "$($prCreationResult.repository.webUrl)/pullrequest/$($prCreationResult.pullRequestId)" + Write-Host "Pull request: $prUrl" + $prApiBaseUri += "/$($prCreationResult.pullRequestId)" + + $SummaryPath = Join-Path '$(Agent.TempDirectory)' 'summary.md' + Set-Content -Path $SummaryPath -Value "[Insertion pull request]($prUrl)" + Write-Host "##vso[task.uploadsummary]$SummaryPath" + + # Tag the PR + $tagUri = "$prApiBaseUri/labels?api-version=7.0" + $rawRequest = @{ + name = 'auto-template-merge'; + } + $request = ConvertTo-Json $rawRequest + Invoke-RestMethod -uri $tagUri -method POST -Headers $headers -ContentType $contentType -Body $request | Out-Null + + # Add properties to the PR that we can programatically parse later. + Function Set-PRProperties($properties) { + $rawRequest = $properties.GetEnumerator() |% { + @{ + op = 'add' + path = "/$($_.key)" + from = $null + value = $_.value + } + } + $request = ConvertTo-Json $rawRequest + $setPrPropertyUri = "$prApiBaseUri/properties?api-version=7.0" + Write-Debug "$request" + $setPrPropertyResult = Invoke-RestMethod -uri $setPrPropertyUri -method PATCH -Headers $headers -ContentType 'application/json-patch+json' -Body $request -StatusCodeVariable setPrPropertyStatus -SkipHttpErrorCheck + if ($setPrPropertyStatus -ne 200) { + Write-Host "##vso[task.logissue type=warning]Failed to set pull request properties. Result: $setPrPropertyStatus. $($setPrPropertyResult.message)" + } + } + Write-Host "Setting pull request properties" + Set-PRProperties @{ + 'AutomatedMerge.SourceBranch' = $LibTemplateBranch + 'AutomatedMerge.SourceCommit' = $LibTemplateCommit + } + + # Add an *active* PR comment to warn users to *merge* the pull request instead of squash it. + $request = ConvertTo-Json @{ + comments = @( + @{ + parentCommentId = 0 + content = "Do **not** squash this pull request when completing it. You must *merge* it." + commentType = 'system' + } + ) + status = 'active' + } + $result = Invoke-RestMethod -uri "$prApiBaseUri/threads?api-version=7.1" -method POST -Headers $headers -ContentType $contentType -Body $request -StatusCodeVariable addCommentStatus -SkipHttpErrorCheck + if ($addCommentStatus -ne 200) { + Write-Host "##vso[task.logissue type=warning]Failed to post comment on pull request. Result: $addCommentStatus. $($result.message)" + } + + # Set auto-complete on the PR + if ('${{ parameters.AutoComplete }}' -eq 'True') { + Write-Host "Setting auto-complete" + $mergeMessage = "Merged PR $($prCreationResult.pullRequestId): " + $commitMessage + $rawRequest = @{ + autoCompleteSetBy = @{ + id = $prCreationResult.createdBy.id + }; + completionOptions = @{ + deleteSourceBranch = $true; + mergeCommitMessage = $mergeMessage; + mergeStrategy = 'noFastForward'; + }; + } + $request = ConvertTo-Json $rawRequest + Write-Host $request + $uri = "$($prApiBaseUri)?api-version=6.0" + $result = Invoke-RestMethod -uri $uri -method PATCH -Headers $headers -ContentType $contentType -Body $request -StatusCodeVariable autoCompleteStatus -SkipHttpErrorCheck + if ($autoCompleteStatus -ne 200) { + Write-Host "##vso[task.logissue type=warning]Failed to set auto-complete on pull request. Result: $autoCompleteStatus. $($result.message)" + } + } + + displayName: Create pull request diff --git a/azure-pipelines/microbuild.after.yml b/azure-pipelines/microbuild.after.yml index 8f6f5257d..c0e1e7bd0 100644 --- a/azure-pipelines/microbuild.after.yml +++ b/azure-pipelines/microbuild.after.yml @@ -1,59 +1,14 @@ parameters: -- name: EnableAPIScan +- name: SkipCodesignVerify type: boolean steps: -- task: MicroBuildCodesignVerify@3 - displayName: Verify Signed Files - inputs: - TargetFolders: | - $(Build.SourcesDirectory)/bin/Packages/$(BuildConfiguration)/NuGet - -- task: MicroBuildCleanup@1 - condition: succeededOrFailed() - displayName: MicroBuild Cleanup - -- task: ms-vscs-artifact.build-tasks.artifactDropTask-1.artifactDropTask@0 - inputs: - dropServiceURI: https://devdiv.artifacts.visualstudio.com - buildNumber: $(ProfilingInputsDropName) - sourcePath: $(Build.ArtifactStagingDirectory)\OptProf\ProfilingInputs - toLowerCase: false - usePat: true - displayName: Publish to Artifact Services - ProfilingInputs - condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) - continueOnError: true - -- task: PublishBuildArtifacts@1 - inputs: - PathtoPublish: $(Build.ArtifactStagingDirectory)/InsertionOutputs - ArtifactName: InsertionOutputs - ArtifactType: Container - displayName: Publish InsertionOutputs as Azure DevOps artifacts - condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) - -- task: ManifestGeneratorTask@0 - displayName: Software Bill of Materials generation - inputs: - BuildDropPath: $(System.DefaultWorkingDirectory)/bin/Microsoft.VisualStudio.Threading/$(BuildConfiguration) - BuildComponentPath: $(System.DefaultWorkingDirectory)/obj/src/Microsoft.VisualStudio.Threading - -- powershell: Copy-Item -Recurse "$(System.DefaultWorkingDirectory)/bin/Microsoft.VisualStudio.Threading/$(BuildConfiguration)/_manifest" "$(System.DefaultWorkingDirectory)/bin/Packages/$(BuildConfiguration)/NuGet" - displayName: Publish Software Bill of Materials - -- task: Ref12Analyze@0 - displayName: Ref12 (Codex) Analyze - inputs: - codexoutputroot: $(Build.ArtifactStagingDirectory)\Codex - workflowArguments: | - /sourcesDirectory:$(Build.SourcesDirectory) - /codexRepoUrl:$(Build.Repository.Uri) - /repoName:$(Build.Repository.Name) - /additionalCodexArguments:-bld - /additionalCodexArguments:$(Build.ArtifactStagingDirectory)/build_logs - condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'), ne(variables['Build.Reason'], 'PullRequest')) - continueOnError: true - -- template: secure-development-tools.yml - parameters: - EnableAPIScan: ${{ parameters.EnableAPIScan }} +- ${{ if not(parameters.SkipCodesignVerify) }}: + - task: MicroBuildCodesignVerify@3 + displayName: 🔍 Verify Signed Files + inputs: + ApprovalListPathForSigs: $(Build.SourcesDirectory)\azure-pipelines\no_strongname.txt + ApprovalListPathForCerts: $(Build.SourcesDirectory)\azure-pipelines\no_authenticode.txt + TargetFolders: | + $(Build.SourcesDirectory)/bin/Packages/$(BuildConfiguration) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) diff --git a/azure-pipelines/microbuild.before.yml b/azure-pipelines/microbuild.before.yml index 900f9d71c..298235321 100644 --- a/azure-pipelines/microbuild.before.yml +++ b/azure-pipelines/microbuild.before.yml @@ -1,31 +1,24 @@ parameters: +- name: EnableLocalization + type: boolean + default: false - name: ShouldSkipOptimize + type: boolean + default: false +- name: RealSign + type: boolean steps: -- task: ComponentGovernanceComponentDetection@0 - displayName: Component Detection +- ${{ if ne(variables['Build.Reason'], 'PullRequest') }}: + # notice@0 requires CG detection to run first, and non-default branches don't inject it automatically. + # default branch injection (main) is happening too late for notice@0 to run successfully. Adding this as a workaround. + - task: ComponentGovernanceComponentDetection@0 + displayName: 🔍 Component Detection -- task: notice@0 - displayName: Generate NOTICE file - inputs: - outputfile: $(System.DefaultWorkingDirectory)/obj/NOTICE - outputformat: text - -- task: MicroBuildOptProfPlugin@6 - inputs: - ProfilingInputsDropName: ProfilingInputs/$(System.TeamProject)/$(Build.Repository.Name)/$(Build.SourceBranchName)/$(Build.BuildNumber) - OptimizationInputsLookupMethod: DropPrefix - DropNamePrefix: OptimizationInputs/$(System.TeamProject)/$(Build.Repository.Name) - ShouldSkipOptimize: ${{ parameters.ShouldSkipOptimize }} - AccessToken: $(System.AccessToken) - displayName: Install OptProf Plugin - -- task: MicroBuildSigningPlugin@3 - inputs: - signType: $(SignType) - displayName: Install MicroBuild Signing Plugin - -- task: MicroBuildLocalizationPlugin@3 - inputs: - languages: $(LocLanguages) - displayName: Install MicroBuild Localization Plugin + - task: notice@0 + displayName: 🛠️ Generate NOTICE file + inputs: + outputfile: $(System.DefaultWorkingDirectory)/obj/NOTICE + outputformat: text + retryCountOnTaskFailure: 10 # fails when the cloud service is overloaded + continueOnError: ${{ not(parameters.RealSign) }} # Tolerate failures when we're not building something that may ship. diff --git a/azure-pipelines/no_authenticode.txt b/azure-pipelines/no_authenticode.txt new file mode 100644 index 000000000..262625ac9 --- /dev/null +++ b/azure-pipelines/no_authenticode.txt @@ -0,0 +1,2 @@ +bin\packages\release\vsix\_manifest\manifest.cat,sbom signed +bin\packages\release\vsix\_manifest\spdx_2.2\manifest.cat,sbom signed diff --git a/azure-pipelines/no_strongname.txt b/azure-pipelines/no_strongname.txt new file mode 100644 index 000000000..e69de29bb diff --git a/azure-pipelines/official.yml b/azure-pipelines/official.yml index 2e2b83508..a63588e31 100644 --- a/azure-pipelines/official.yml +++ b/azure-pipelines/official.yml @@ -1,122 +1,90 @@ -trigger: - batch: true - branches: - include: - - main - - 'v16.*' - - 'v17.*' - - 'validate/*' - paths: - exclude: - - .github/ - - doc/ - - '*.md' - - .vscode/ +trigger: none # We only want to trigger manually or based on a schedule +pr: none schedules: - cron: "0 3 * * *" # Daily @ 8 PM PST displayName: Daily vs-insertion branches: include: - main - - 'v16.*' - - 'v17.*' parameters: -- name: SignTypeSelection - displayName: Sign type - type: string - default: Test - values: [ 'Test', 'Real' ] +# As an entrypoint pipeline yml file, all parameters here show up in the Queue Run dialog. +# If any paramaters should NOT be queue-time options, they should be removed from here +# and references to them in this file replaced with hard-coded values. - name: ShouldSkipOptimize displayName: Skip OptProf optimization type: boolean default: false -- name: includeMacOS +- name: EnableMacOSBuild displayName: Build on macOS type: boolean default: false # macOS is often bogged down in Azure Pipelines +- name: RunTests + displayName: Run tests + type: boolean + default: true - name: EnableAPIScan - displayName: Run APIScan + displayName: Include APIScan with compliance tools type: boolean - default: false # enable when we get it passing - -variables: - NugetSecurityAnalysisWarningLevel: none # nuget.config requires signed packages by trusted owners - -stages: - -- stage: Build - variables: - TreatWarningsAsErrors: true - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true - BuildConfiguration: Release - push_to_ci: true - NUGET_PACKAGES: $(Agent.TempDirectory)/.nuget/packages - SignTypeSelection: ${{ parameters.SignTypeSelection }} - Packaging.EnableSBOMSigning: true + default: true +- name: PublishCodeCoverage + displayName: Publish code coverage + type: boolean + default: true - jobs: - - template: build.yml - parameters: - EnableAPIScan: ${{ parameters.EnableAPIScan }} - windowsPool: VSEngSS-MicroBuild2022-1ES - ShouldSkipOptimize: ${{ parameters.ShouldSkipOptimize }} - includeMacOS: ${{ parameters.includeMacOS }} +resources: + repositories: + - repository: MicroBuildTemplate + type: git + name: 1ESPipelineTemplates/MicroBuildTemplate + ref: refs/tags/release -- stage: symbol_archive - displayName: Symbol archival - condition: and(succeeded(), eq(dependencies.Build.outputs['Windows.SetPipelineVariables.SignType'], 'Real')) - jobs: - - job: archive - pool: VSEng-ReleasePool-1ES - steps: - - download: current - artifact: Variables-Windows - displayName: Download Variables-Windows artifact - - task: PowerShell@2 - displayName: Set VSTS variables based on artifacts - inputs: - targetType: filePath - filePath: $(Pipeline.Workspace)/Variables-Windows/_pipelines.ps1 - - download: current - artifact: symbols-Windows - displayName: Download symbols-Windows artifact - - task: MicroBuildArchiveSymbols@1 - displayName: Archive symbols to Symweb - inputs: - SymbolsFeatureName: $(SymbolsFeatureName) - SymbolsSymwebProject: VS - SymbolsUncPath: \\cpvsbuild\drops\$(TeamName)\$(Build.DefinitionName)\$(Build.SourceBranchName)\$(Build.BuildId)\Symbols.Archival - SymbolsEmailContacts: vsidemicrobuild - SymbolsAgentPath: $(Pipeline.Workspace)/symbols-Windows - - task: MicroBuildCleanup@1 - displayName: Send Telemetry +variables: +- template: GlobalVariables.yml -- stage: azure_public_vssdk_feed - displayName: azure-public/vssdk feed - condition: and(succeeded(), eq(dependencies.Build.outputs['Windows.SetPipelineVariables.SignType'], 'Real')) - jobs: - - deployment: push - pool: - vmImage: ubuntu-latest - environment: No-Approval - strategy: - runOnce: - deploy: - steps: - - download: current - artifact: deployables-Windows - displayName: Download deployables-Windows artifact - - task: NuGetToolInstaller@1 - displayName: Use NuGet 5.x - inputs: - versionSpec: 5.x - - task: NuGetCommand@2 - displayName: NuGet push - inputs: - command: push - packagesToPush: $(Pipeline.Workspace)/deployables-Windows/NuGet/*.nupkg - nuGetFeedType: external - publishFeedCredentials: azure-public/vssdk - allowPackageConflicts: true - continueOnError: true # until "skip on conflict" is offered as a task input. +extends: + template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate + parameters: + settings: + networkIsolationPolicy: Permissive,CFSClean2 + sdl: + sourceAnalysisPool: VSEng-MicroBuildVSStable + codeSignValidation: + enabled: true + break: true + additionalTargetsGlobPattern: -|Variables-*\*.ps1;-|LocBin-*\**;-|APIScanInputs-*\**;-|test_symbols-*\**;-|MicroBuild\**;-|$(Build.ArtifactStagingDirectory)\VSInsertion-Windows\vs-insertion-script.ps1 + policheck: + enabled: true + exclusionsFile: $(System.DefaultWorkingDirectory)\azure-pipelines\PoliCheckExclusions.xml + suppression: + suppressionFile: $(System.DefaultWorkingDirectory)\azure-pipelines\falsepositives.gdnsuppress + sbom: + enabled: false # Skip 1ES SBOM because microbuild has our own sbom system + stages: + - stage: Build + variables: + - template: /azure-pipelines/BuildStageVariables.yml@self + jobs: + - template: /azure-pipelines/build.yml@self + parameters: + Is1ESPT: true + Is1ESPTOfficial: true + RealSign: true + ShouldSkipOptimize: ${{ parameters.ShouldSkipOptimize }} + EnableAPIScan: ${{ parameters.EnableAPIScan }} + windowsPool: VSEng-MicroBuildVSStable + linuxPool: + name: AzurePipelines-EO + demands: + - ImageOverride -equals 1ESPT-Ubuntu24.04 + os: Linux + macOSPool: + name: Azure Pipelines + vmImage: macOS-15 + os: macOS + EnableMacOSBuild: ${{ parameters.EnableMacOSBuild }} + RunTests: ${{ parameters.RunTests }} + PublishCodeCoverage: ${{ parameters.PublishCodeCoverage }} + - template: /azure-pipelines/prepare-insertion-stages.yml@self + parameters: + RealSign: true diff --git a/azure-pipelines/prepare-insertion-stages.yml b/azure-pipelines/prepare-insertion-stages.yml new file mode 100644 index 000000000..e127a146f --- /dev/null +++ b/azure-pipelines/prepare-insertion-stages.yml @@ -0,0 +1,100 @@ +parameters: +- name: ArchiveSymbols + type: boolean + default: true +- name: RealSign + displayName: Real sign? + type: boolean +- name: PackagePush + type: boolean + default: true + +stages: +- ${{ if or(parameters.ArchiveSymbols, parameters.PackagePush) }}: + - stage: release + displayName: Publish + jobs: + - ${{ if parameters.ArchiveSymbols }}: + - job: symbol_archive + displayName: Archive symbols + pool: VSEng-MicroBuildVSStable + variables: + ONEES_ENFORCED_CODEQL_ENABLED: false # CodeQL runs on build stages, we don't need it here + steps: + - checkout: none + - download: current + artifact: Variables-Windows + displayName: 🔻 Download Variables-Windows artifact + - powershell: $(Pipeline.Workspace)/Variables-Windows/_define.ps1 + displayName: ⚙️ Set pipeline variables based on artifacts + - download: current + artifact: symbols-legacy + displayName: 🔻 Download symbols-legacy artifact + - task: MicroBuildArchiveSymbols@6 + displayName: 🔣 Archive symbols to Symweb + inputs: + SymbolsFeatureName: $(SymbolsFeatureName) + SymbolsProject: VS + SymbolsAgentPath: $(Pipeline.Workspace)/symbols-legacy + azureSubscription: Vseng-SymbolsUpload + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + + - ${{ if parameters.PackagePush }}: + - job: push + ${{ if parameters.RealSign }}: + displayName: azure-public feeds + ${{ else }}: + displayName: devdiv/vs-impl feed # Leave this as-is, since non-signed builds must not be pushed to public feeds. + ${{ if parameters.ArchiveSymbols }}: + dependsOn: symbol_archive + pool: + name: AzurePipelines-EO + demands: + - ImageOverride -equals 1ESPT-Ubuntu22.04 # Do NOT upgrade this job to Ubuntu24 until this is fixed: https://portal.microsofticm.com/imp/v5/incidents/details/830879871/summary + os: Linux + templateContext: + outputs: + - output: nuget + displayName: 📦 Push nuget packages + packagesToPush: '$(Pipeline.Workspace)/deployables-Windows/NuGet/*.nupkg' + packageParentPath: $(Pipeline.Workspace)/deployables-Windows/NuGet + allowPackageConflicts: true + ${{ if parameters.RealSign }}: + nuGetFeedType: external + publishFeedCredentials: azure-public/vssdk + ${{ else }}: + nuGetFeedType: internal + publishVstsFeed: vs-impl # Leave this as-is, since non-signed builds must not be pushed to public feeds. + - output: nuget + displayName: 📦 Push WinDBG extension + packagesToPush: '$(Pipeline.Workspace)/deployables-Windows/WinDBGGallery/*.nupkg' + packageParentPath: $(Pipeline.Workspace)/deployables-Windows/WinDBGGallery + allowPackageConflicts: true + ${{ if parameters.RealSign }}: + nuGetFeedType: external + publishFeedCredentials: azure-public/vs-impl + ${{ else }}: + nuGetFeedType: internal + publishVstsFeed: vs-impl # Leave this as-is, since non-signed builds must not be pushed to public feeds. + variables: + ONEES_ENFORCED_CODEQL_ENABLED: false # CodeQL runs on build stages, we don't need it here + steps: + - checkout: none + - download: current + artifact: Variables-Windows + displayName: 🔻 Download Variables-Windows artifact + - powershell: $(Pipeline.Workspace)/Variables-Windows/_define.ps1 + displayName: ⚙️ Set pipeline variables based on artifacts + - download: current + artifact: deployables-Windows + displayName: 🔻 Download deployables-Windows artifact + - ${{ if parameters.RealSign }}: + - template: WIFtoPATauth.yml + parameters: + wifServiceConnectionName: azure-public/vside package push + deadPATServiceConnectionId: 42175e93-c771-4a4f-a132-3cca78f44b3b # azure-public/vssdk + - template: WIFtoPATauth.yml + parameters: + wifServiceConnectionName: azure-public/vside package push + deadPATServiceConnectionId: 207efd62-fd0f-43e7-aeae-17c4febcc660 # azure-public/vs-impl diff --git a/azure-pipelines/publish-codecoverage.yml b/azure-pipelines/publish-codecoverage.yml index a0862be3f..987b2fe23 100644 --- a/azure-pipelines/publish-codecoverage.yml +++ b/azure-pipelines/publish-codecoverage.yml @@ -1,35 +1,28 @@ parameters: - includeMacOS: +- name: EnableMacOSBuild + type: boolean +- name: EnableLinuxBuild + type: boolean steps: - download: current artifact: coverageResults-Windows - displayName: Download Windows code coverage results + displayName: 🔻 Download Windows code coverage results continueOnError: true -- download: current - artifact: coverageResults-Linux - displayName: Download Linux code coverage results - continueOnError: true -- download: current - artifact: coverageResults-macOS - displayName: Download macOS code coverage results - continueOnError: true - condition: ${{ parameters.includeMacOS }} -- powershell: | - dotnet tool install --tool-path obj dotnet-reportgenerator-globaltool --version 4.8.5 --configfile azure-pipelines/justnugetorg.nuget.config - Copy-Item -Recurse $(Pipeline.Workspace)/coverageResults-Windows/obj/* $(System.DefaultWorkingDirectory)/obj - Write-Host 'Substituting {reporoot} with $(System.DefaultWorkingDirectory)' - $reports = Get-ChildItem -Recurse '$(Pipeline.Workspace)/coverage.*cobertura.xml' - $reports |% { - $content = Get-Content -Path $_ |% { $_.Replace('{reporoot}', '$(System.DefaultWorkingDirectory)') } - Set-Content -Path $_ -Value $content -Encoding UTF8 - } - $Inputs = [string]::join(';', ($reports |% { Resolve-Path -relative $_ })) - obj/reportgenerator -reports:"$Inputs" -targetdir:coveragereport -reporttypes:Cobertura - displayName: Merge coverage -- task: PublishCodeCoverageResults@1 - displayName: Publish code coverage results to Azure DevOps +- ${{ if parameters.EnableLinuxBuild }}: + - download: current + artifact: coverageResults-Linux + displayName: 🔻 Download Linux code coverage results + continueOnError: true +- ${{ if parameters.EnableMacOSBuild }}: + - download: current + artifact: coverageResults-macOS + displayName: 🔻 Download macOS code coverage results + continueOnError: true +- powershell: azure-pipelines/Merge-CodeCoverage.ps1 -Path '$(Pipeline.Workspace)' -OutputFile coveragereport/merged.cobertura.xml -Format Cobertura -Verbose + displayName: ⚙ Merge coverage +- task: PublishCodeCoverageResults@2 + displayName: 📢 Publish code coverage results to Azure DevOps inputs: - codeCoverageTool: cobertura - summaryFileLocation: 'coveragereport/Cobertura.xml' + summaryFileLocation: coveragereport/merged.cobertura.xml failIfCoverageEmpty: true diff --git a/azure-pipelines/publish-symbols.yml b/azure-pipelines/publish-symbols.yml new file mode 100644 index 000000000..e2ce081ef --- /dev/null +++ b/azure-pipelines/publish-symbols.yml @@ -0,0 +1,67 @@ +parameters: +- name: EnableMacOSBuild + type: boolean +- name: EnableLinuxBuild + type: boolean + +steps: +- task: DownloadPipelineArtifact@2 + inputs: + artifact: symbols-Windows + path: $(Pipeline.Workspace)/symbols/Windows + displayName: 🔻 Download Windows symbols + continueOnError: true +- ${{ if parameters.EnableLinuxBuild }}: + - task: DownloadPipelineArtifact@2 + inputs: + artifact: symbols-Linux + path: $(Pipeline.Workspace)/symbols/Linux + displayName: 🔻 Download Linux symbols + continueOnError: true +- ${{ if parameters.EnableMacOSBuild }}: + - task: DownloadPipelineArtifact@2 + inputs: + artifact: symbols-macOS + path: $(Pipeline.Workspace)/symbols/macOS + displayName: 🔻 Download macOS symbols + continueOnError: true + +- task: DownloadPipelineArtifact@2 + inputs: + artifact: test_symbols-Windows + path: $(Pipeline.Workspace)/test_symbols/Windows + displayName: 🔻 Download Windows test symbols + continueOnError: true +- ${{ if parameters.EnableLinuxBuild }}: + - task: DownloadPipelineArtifact@2 + inputs: + artifact: test_symbols-Linux + path: $(Pipeline.Workspace)/test_symbols/Linux + displayName: 🔻 Download Linux test symbols + continueOnError: true +- ${{ if parameters.EnableMacOSBuild }}: + - task: DownloadPipelineArtifact@2 + inputs: + artifact: test_symbols-macOS + path: $(Pipeline.Workspace)/test_symbols/macOS + displayName: 🔻 Download macOS test symbols + continueOnError: true + +- task: PublishSymbols@2 + inputs: + SymbolsFolder: $(Pipeline.Workspace)/symbols + SearchPattern: '**/*.pdb' + IndexSources: false + SymbolServerType: TeamServices + displayName: 📢 Publish symbols + +- task: PublishSymbols@2 + inputs: + SymbolsFolder: $(Pipeline.Workspace)/test_symbols + SearchPattern: '**/*.pdb' + IndexSources: false + SymbolServerType: TeamServices + displayName: 📢 Publish test symbols + +- powershell: tools/Prepare-Legacy-Symbols.ps1 -Path $(Pipeline.Workspace)/symbols/Windows + displayName: ⚙ Prepare symbols for symbol archival diff --git a/azure-pipelines/publish_artifacts.ps1 b/azure-pipelines/publish_artifacts.ps1 new file mode 100644 index 000000000..3f35cc6e1 --- /dev/null +++ b/azure-pipelines/publish_artifacts.ps1 @@ -0,0 +1,45 @@ +<# +.SYNOPSIS + This script translates all the artifacts described by _all.ps1 + into commands that instruct Azure Pipelines to actually collect those artifacts. +#> + +[CmdletBinding()] +param ( + [string]$ArtifactNameSuffix, + [switch]$StageOnly, + [switch]$AvoidSymbolicLinks +) + +Function Set-PipelineVariable($name, $value) { + if ((Test-Path "Env:\$name") -and (Get-Item "Env:\$name").Value -eq $value) { + return # already set + } + + #New-Item -LiteralPath "Env:\$name".ToUpper() -Value $value -Force | Out-Null + Write-Host "##vso[task.setvariable variable=$name]$value" +} + +Function Test-ArtifactUploaded($artifactName) { + $varName = "ARTIFACTUPLOADED_$($artifactName.ToUpper())" + Test-Path "env:$varName" +} + +& "$PSScriptRoot/../tools/artifacts/_stage_all.ps1" -ArtifactNameSuffix $ArtifactNameSuffix -AvoidSymbolicLinks:$AvoidSymbolicLinks |% { + # Set a variable which will out-live this script so that a subsequent attempt to collect and upload artifacts + # will skip this one from a check in the _all.ps1 script. + Set-PipelineVariable "ARTIFACTSTAGED_$($_.Name.ToUpper())" 'true' + Write-Host "Staged artifact $($_.Name) to $($_.Path)" + + if (!$StageOnly) { + if (Test-ArtifactUploaded $_.Name) { + Write-Host "Skipping $($_.Name) because it has already been uploaded." -ForegroundColor DarkGray + } else { + Write-Host "##vso[artifact.upload containerfolder=$($_.Name);artifactname=$($_.Name);]$($_.Path)" + + # Set a variable which will out-live this script so that a subsequent attempt to collect and upload artifacts + # will skip this one from a check in the _all.ps1 script. + Set-PipelineVariable "ARTIFACTUPLOADED_$($_.Name.ToUpper())" 'true' + } + } +} diff --git a/azure-pipelines/release-deployment-prep.yml b/azure-pipelines/release-deployment-prep.yml index 6dee28e5a..17008b598 100644 --- a/azure-pipelines/release-deployment-prep.yml +++ b/azure-pipelines/release-deployment-prep.yml @@ -1,9 +1,6 @@ steps: - download: CI artifact: Variables-Windows - displayName: Download Variables-Windows artifact -- task: PowerShell@2 - displayName: Set VSTS variables based on artifacts - inputs: - targetType: filePath - filePath: $(Pipeline.Workspace)/CI/Variables-Windows/_pipelines.ps1 + displayName: 🔻 Download Variables-Windows artifact +- powershell: $(Pipeline.Workspace)/CI/Variables-Windows/_define.ps1 + displayName: ⚙️ Set pipeline variables based on artifacts diff --git a/azure-pipelines/release.yml b/azure-pipelines/release.yml index 46fb6c0e0..e7e804b60 100644 --- a/azure-pipelines/release.yml +++ b/azure-pipelines/release.yml @@ -2,6 +2,11 @@ trigger: none # We only want to trigger manually or based on resources pr: none resources: + repositories: + - repository: MicroBuildTemplate + type: git + name: 1ESPipelineTemplates/MicroBuildTemplate + ref: refs/tags/release pipelines: - pipeline: CI source: vs-threading @@ -9,74 +14,87 @@ resources: tags: - auto-release -stages: -- stage: GitHubRelease - displayName: GitHub Release - jobs: - - deployment: create - pool: - vmImage: ubuntu-latest - environment: No-Approval - strategy: - runOnce: - deploy: - steps: - - download: CI - artifact: deployables-Windows - displayName: Download deployables-Windows artifact - - powershell: | - Write-Host "##vso[build.updatebuildnumber]$(resources.pipeline.CI.runName)" - displayName: Set pipeline name - - task: GitHubRelease@1 - displayName: GitHub release (create) - inputs: - gitHubConnection: AArnott - repositoryName: $(Build.Repository.Name) - target: $(resources.pipeline.CI.sourceCommit) - tagSource: userSpecifiedTag - tag: v$(resources.pipeline.CI.runName) - title: v$(resources.pipeline.CI.runName) - assets: | - $(Pipeline.Workspace)/CI/deployables-Windows/SosThreadingTools.zip - isDraft: true # After running this step, visit the new draft release, edit, and publish. - changeLogCompareToRelease: lastNonDraftRelease - changeLogType: issueBased - changeLogLabels: | - [ - { "label" : "bug", "displayName" : "Fixes", "state" : "closed" }, - { "label" : "enhancement", "displayName": "Enhancements", "state" : "closed" } - ] +variables: +- template: GlobalVariables.yml -- stage: nuget_org - displayName: nuget.org - dependsOn: GitHubRelease - jobs: - - deployment: push - pool: - vmImage: ubuntu-latest - environment: No-Approval - strategy: - runOnce: - deploy: - steps: - - download: CI - artifact: deployables-Windows - displayName: Download deployables-Windows artifact - - task: NuGetToolInstaller@1 - displayName: Use NuGet 5.x - inputs: - versionSpec: 5.x - - task: NuGetCommand@2 - displayName: NuGet push - inputs: - command: push - packagesToPush: $(Pipeline.Workspace)/CI/deployables-Windows/NuGet/*.nupkg - nuGetFeedType: external - publishFeedCredentials: VisualStudioExtensibility (nuget.org) - - task: NuGetCommand@2 - displayName: WinDBG Gallery - inputs: - command: push - packagesToPush: $(Pipeline.Workspace)/CI/deployables-Windows/WinDBGGallery/*.nupkg - nuGetFeedType: external - publishFeedCredentials: microsoft package push (andarno) +extends: + template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate + parameters: + settings: + networkIsolationPolicy: Permissive,CFSClean2 + sdl: + sourceAnalysisPool: VSEng-MicroBuildVSStable + + stages: + - stage: release + jobs: + - job: nuget + displayName: 📦 Push nuget.org packages + pool: + name: AzurePipelines-EO + demands: + - ImageOverride -equals 1ESPT-Ubuntu22.04 # Do NOT upgrade this job to Ubuntu24 until this is fixed: https://portal.microsofticm.com/imp/v5/incidents/details/830879871/summary + os: Linux + templateContext: + outputs: + - output: nuget + displayName: 📦 Push packages to nuget.org + packagesToPush: '$(Pipeline.Workspace)/CI/deployables-Windows/NuGet/*.nupkg' + packageParentPath: $(Pipeline.Workspace)/CI/deployables-Windows/NuGet + allowPackageConflicts: true + nuGetFeedType: external + publishFeedCredentials: VisualStudioExtensibility (nuget.org) + steps: + - checkout: none + - download: CI + artifact: deployables-Windows + displayName: 🔻 Download deployables-Windows artifact + patterns: 'NuGet/*' + - job: github + displayName: 📢 GitHub release + dependsOn: nuget + pool: + name: AzurePipelines-EO + demands: + - ImageOverride -equals 1ESPT-Ubuntu24.04 + os: Linux + templateContext: + type: releaseJob + isProduction: true + inputs: + - input: pipelineArtifact + pipeline: CI + artifactName: deployables-Windows + targetPath: $(Pipeline.Workspace)/CI/deployables-Windows + steps: + - checkout: none + - powershell: | + Write-Host "##vso[build.updatebuildnumber]$(resources.pipeline.CI.runName)" + if ('$(resources.pipeline.CI.runName)'.Contains('-')) { + Write-Host "##vso[task.setvariable variable=IsPrerelease]true" + } else { + Write-Host "##vso[task.setvariable variable=IsPrerelease]false" + } + displayName: ⚙ Set up pipeline + - task: GitHubRelease@1 + displayName: 📢 GitHub release (create) + inputs: + gitHubConnection: AArnott + repositoryName: $(Build.Repository.Name) + target: $(resources.pipeline.CI.sourceCommit) + tagSource: userSpecifiedTag + tag: v$(resources.pipeline.CI.runName) + title: v$(resources.pipeline.CI.runName) + isDraft: true # After running this step, visit the new draft release, edit, and publish. + isPreRelease: $(IsPrerelease) + assets: | + $(Pipeline.Workspace)/CI/deployables-Windows/NuGet/*.nupkg + $(Pipeline.Workspace)/CI/deployables-Windows/WinDBGGallery/*.nupkg + changeLogCompareToRelease: lastNonDraftRelease + changeLogType: issueBased + changeLogLabels: | + [ + { "label" : "breaking change", "displayName" : "Breaking changes", "state" : "closed" }, + { "label" : "bug", "displayName" : "Fixes", "state" : "closed" }, + { "label" : "enhancement", "displayName": "Enhancements", "state" : "closed" } + ] diff --git a/azure-pipelines/richnav.yml b/azure-pipelines/richnav.yml deleted file mode 100644 index 716184f6f..000000000 --- a/azure-pipelines/richnav.yml +++ /dev/null @@ -1,20 +0,0 @@ -jobs: -- job: RichCodeNavUpload - displayName: Rich Code Navigation Upload to Production - pool: - vmImage: windows-2022 - steps: - - template: install-dependencies.yml - - task: VSBuild@1 - displayName: Build Visual Studio solution - inputs: - msbuildArgs: /t:build /m /bl:"$(Build.ArtifactStagingDirectory)/build_logs/msbuild.binlog" - platform: Any CPU - configuration: $(BuildConfiguration) - - task: RichCodeNavIndexer@0 - displayName: RichCodeNav Upload - inputs: - languages: 'csharp' - environment: production - isPrivateFeed: false - continueOnError: true diff --git a/azure-pipelines/schedule-only-steps.yml b/azure-pipelines/schedule-only-steps.yml deleted file mode 100644 index ad07a341b..000000000 --- a/azure-pipelines/schedule-only-steps.yml +++ /dev/null @@ -1,3 +0,0 @@ -steps: -- powershell: echo "##vso[build.addbuildtag]auto-insertion" - displayName: Tag for auto-insertion diff --git a/azure-pipelines/secure-development-tools.yml b/azure-pipelines/secure-development-tools.yml deleted file mode 100644 index 99925c13d..000000000 --- a/azure-pipelines/secure-development-tools.yml +++ /dev/null @@ -1,74 +0,0 @@ -parameters: -- name: EnableAPIScan - type: boolean - -steps: - -- task: CredScan@3 - displayName: Run CredScan - -- task: PoliCheck@2 - displayName: Run PoliCheck - inputs: - targetType: F - targetArgument: $(System.DefaultWorkingDirectory) - optionsUEPATH: $(System.DefaultWorkingDirectory)\azure-pipelines\PoliCheckExclusions.xml - -- task: BinSkim@3 - displayName: Run BinSkim - inputs: - InputType: Basic - Function: analyze - AnalyzeTarget: $(BinSkimTargets) - -- task: CopyFiles@2 - displayName: Collect APIScan inputs - inputs: - SourceFolder: $(Build.ArtifactStagingDirectory)/Symbols-$(Agent.JobName) - # Exclude any patterns from the Contents (e.g. `!**/git2*`) that we have symbols for but do not need to run APIScan on. - Contents: | - ** - TargetFolder: $(Build.ArtifactStagingDirectory)/APIScanInputs - condition: and(succeeded(), ${{ parameters.EnableAPIScan }}, ne(variables.ApiScanClientId, '')) - -- task: APIScan@2 - displayName: Run APIScan - inputs: - softwareFolder: $(Build.ArtifactStagingDirectory)/APIScanInputs - softwareName: $(SymbolsFeatureName) - softwareVersionNum: $(NBGV_MajorMinorVersion) - isLargeApp: false - toolVersion: Latest - condition: and(succeeded(), ${{ parameters.EnableAPIScan }}, ne(variables.ApiScanClientId, '')) - env: - AzureServicesAuthConnectionString: runAs=App;AppId=$(ApiScanClientId);TenantId=$(ApiScanTenant);AppKey=$(ApiScanSecret) - -- task: SdtReport@2 - displayName: Create Security Analysis Report - inputs: - GdnExportAllTools: true - -- task: PublishSecurityAnalysisLogs@3 - displayName: Publish Code Analysis Logs - inputs: - ArtifactName: CodeAnalysisLogs - ArtifactType: Container - PublishProcessedResults: true - AllTools: true - ToolLogsNotFoundAction: Standard - -- task: PostAnalysis@2 - displayName: Break on compliance issues - inputs: - GdnBreakAllTools: true - GdnBreakGdnToolBinSkimSeverity: Warning - GdnBreakSuppressionFiles: $(System.DefaultWorkingDirectory)/azure-pipelines/falsepositives.gdnsuppress - GdnBreakSuppressionSets: falsepositives - GdnBreakOutputSuppressionFile: $(Build.ArtifactStagingDirectory)/guardian_failures_as_suppressions/ - GdnBreakOutputSuppressionSet: falsepositives - -# This is useful when false positives appear so we can copy some of the output into the suppressions file. -- publish: $(Build.ArtifactStagingDirectory)/guardian_failures_as_suppressions - artifact: guardian_failures_as_suppressions - displayName: Publish Guardian failures - condition: failed() diff --git a/azure-pipelines/unofficial.yml b/azure-pipelines/unofficial.yml new file mode 100644 index 000000000..7a968fd15 --- /dev/null +++ b/azure-pipelines/unofficial.yml @@ -0,0 +1,101 @@ +trigger: + batch: true + branches: + include: + - main + - 'v16.*' + - 'v17.*' + - 'validate/*' + paths: + exclude: + - doc/ + - '*.md' + - .vscode/ + - azure-pipelines/release.yml + - azure-pipelines/vs-insertion.yml + +parameters: +# As an entrypoint pipeline yml file, all parameters here show up in the Queue Run dialog. +# If any paramaters should NOT be queue-time options, they should be removed from here +# and references to them in this file replaced with hard-coded values. +- name: ShouldSkipOptimize + displayName: Skip OptProf optimization + type: boolean + default: false +- name: EnableMacOSBuild + displayName: Build on macOS + type: boolean + default: false # macOS is often bogged down in Azure Pipelines +- name: RunTests + displayName: Run tests + type: boolean + default: true +- name: EnableAPIScan + displayName: Include APIScan with compliance tools + type: boolean + default: false +- name: EnableProductionSDL + displayName: Enable Production SDL + type: boolean + default: false +- name: PublishCodeCoverage + displayName: Publish code coverage + type: boolean + default: true + +resources: + repositories: + - repository: MicroBuildTemplate + type: git + name: 1ESPipelineTemplates/MicroBuildTemplate + ref: refs/tags/release + +variables: +- template: GlobalVariables.yml + +extends: + template: azure-pipelines/MicroBuild.1ES.Unofficial.yml@MicroBuildTemplate + parameters: + settings: + networkIsolationPolicy: Permissive,CFSClean2 + sdl: + sourceAnalysisPool: VSEng-MicroBuildVSStable + credscan: + enabled: false + suppression: + suppressionFile: $(System.DefaultWorkingDirectory)\azure-pipelines\falsepositives.gdnsuppress + enableProductionSDL: ${{ parameters.EnableProductionSDL }} + codeSignValidation: + enabled: ${{ parameters.EnableProductionSDL }} + break: true + additionalTargetsGlobPattern: -|Variables-*\*.ps1;-|APIScanInputs-*\**;-|test_symbols-*\**;-|MicroBuild\**;-|$(Build.ArtifactStagingDirectory)\VSInsertion-Windows\vs-insertion-script.ps1 + policyFile: $(MBSIGN_APPFOLDER)\CSVTestSignPolicy.xml + policheck: + enabled: ${{ parameters.EnableProductionSDL }} + exclusionsFile: $(System.DefaultWorkingDirectory)\azure-pipelines\PoliCheckExclusions.xml + sbom: + enabled: false # Skip 1ES SBOM because microbuild has our own sbom system + stages: + - stage: Build + variables: + - template: /azure-pipelines/BuildStageVariables.yml@self + jobs: + - template: /azure-pipelines/build.yml@self + parameters: + Is1ESPT: true + RealSign: false + ShouldSkipOptimize: ${{ parameters.ShouldSkipOptimize }} + EnableAPIScan: ${{ parameters.EnableAPIScan }} + windowsPool: VSEng-MicroBuildVSStable + linuxPool: + name: AzurePipelines-EO + demands: + - ImageOverride -equals 1ESPT-Ubuntu24.04 + os: Linux + macOSPool: + name: Azure Pipelines + vmImage: macOS-15 + os: macOS + EnableMacOSBuild: ${{ parameters.EnableMacOSBuild }} + RunTests: ${{ parameters.RunTests }} + PublishCodeCoverage: ${{ parameters.PublishCodeCoverage }} diff --git a/azure-pipelines/variables/BinSkimTargets.ps1 b/azure-pipelines/variables/BinSkimTargets.ps1 deleted file mode 100644 index 5c0dd24ec..000000000 --- a/azure-pipelines/variables/BinSkimTargets.ps1 +++ /dev/null @@ -1,4 +0,0 @@ -$Path = "$PSScriptRoot\..\..\bin" -if (Test-Path $Path) { - [string]::join(';', (& "$PSScriptRoot\..\Get-SymbolFiles.ps1" -ConvertToWindowsPDBs:$false -Path $Path)) -} diff --git a/azure-pipelines/variables/DotNetSdkVersion.ps1 b/azure-pipelines/variables/DotNetSdkVersion.ps1 deleted file mode 100644 index b213fbc27..000000000 --- a/azure-pipelines/variables/DotNetSdkVersion.ps1 +++ /dev/null @@ -1,2 +0,0 @@ -$globalJson = Get-Content -Path "$PSScriptRoot\..\..\global.json" | ConvertFrom-Json -$globalJson.sdk.version diff --git a/azure-pipelines/variables/InsertConfigValues.ps1 b/azure-pipelines/variables/InsertConfigValues.ps1 deleted file mode 100644 index a8aafb460..000000000 --- a/azure-pipelines/variables/InsertConfigValues.ps1 +++ /dev/null @@ -1,15 +0,0 @@ -$BinPath = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..\bin\Packages\$env:BUILDCONFIGURATION") - -$dirsToSearch = "$BinPath\NuGet\*.nupkg","$BinPath\CoreXT\*.nupkg" |? { Test-Path $_ } -$icv=@() -if ($dirsToSearch) { - Get-ChildItem -Path $dirsToSearch |% { - if ($_.Name -match "^(.*?)\.(\d+\.\d+\.\d+(?:\.\d+)?(?:-.*?)?)(?:\.symbols)?\.nupkg$") { - $id = $Matches[1] - $version = $Matches[2] - $icv += "$id=$version" - } - } -} - -Write-Output ([string]::join(',',$icv)) diff --git a/azure-pipelines/variables/InsertReviewers.ps1 b/azure-pipelines/variables/InsertReviewers.ps1 deleted file mode 100644 index 67ec2d899..000000000 --- a/azure-pipelines/variables/InsertReviewers.ps1 +++ /dev/null @@ -1 +0,0 @@ -'Andrew Arnott' diff --git a/azure-pipelines/variables/InsertVersionsValues.ps1 b/azure-pipelines/variables/InsertVersionsValues.ps1 deleted file mode 100644 index 3deacf492..000000000 --- a/azure-pipelines/variables/InsertVersionsValues.ps1 +++ /dev/null @@ -1,4 +0,0 @@ -$nbgv = & "$PSScriptRoot\..\Get-nbgv.ps1" -[string]::join(',',(@{ - 'MicrosoftVisualStudioThreadingVersion' = & { (& $nbgv get-version --project "$PSScriptRoot\..\..\src\Microsoft.VisualStudio.Threading" --format json | ConvertFrom-Json).AssemblyVersion }; -}.GetEnumerator() |% { "$($_.key)=$($_.value)" })) diff --git a/azure-pipelines/variables/SignType.ps1 b/azure-pipelines/variables/SignType.ps1 deleted file mode 100644 index 0c1a335aa..000000000 --- a/azure-pipelines/variables/SignType.ps1 +++ /dev/null @@ -1,11 +0,0 @@ -if ($env:SYSTEM_COLLECTIONID -eq '011b8bdf-6d56-4f87-be0d-0092136884d9') { - if ($env:BUILD_REASON -eq 'Schedule') { - 'real' - } else { - if ($env:SIGNTYPESELECTION) { - $env:SIGNTYPESELECTION - } else { - 'test' - } - } - } diff --git a/azure-pipelines/variables/TeamEmail.ps1 b/azure-pipelines/variables/TeamEmail.ps1 deleted file mode 100644 index 7cf66982b..000000000 --- a/azure-pipelines/variables/TeamEmail.ps1 +++ /dev/null @@ -1 +0,0 @@ -'vsidemicrobuild@microsoft.com' diff --git a/azure-pipelines/variables/TeamName.ps1 b/azure-pipelines/variables/TeamName.ps1 deleted file mode 100644 index 5f2822c50..000000000 --- a/azure-pipelines/variables/TeamName.ps1 +++ /dev/null @@ -1,2 +0,0 @@ -# This value is used to craft a \\cpvsbuild\drops path for symbol archival. -'VS IDE' diff --git a/azure-pipelines/variables/_all.ps1 b/azure-pipelines/variables/_all.ps1 deleted file mode 100755 index 0407d307e..000000000 --- a/azure-pipelines/variables/_all.ps1 +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env pwsh - -# This script returns a hashtable of build variables that should be set -# at the start of a build or release definition's execution. - -$vars = @{} - -Get-ChildItem "$PSScriptRoot\*.ps1" -Exclude "_*" |% { - Write-Host "Computing $($_.BaseName) variable" - $vars[$_.BaseName] = & $_ -} - -$vars diff --git a/azure-pipelines/vs-insertion-script.ps1 b/azure-pipelines/vs-insertion-script.ps1 new file mode 100644 index 000000000..53076254f --- /dev/null +++ b/azure-pipelines/vs-insertion-script.ps1 @@ -0,0 +1,17 @@ +# List of build artifact files [Source => Destination] to be committed into the VS repo. +$FilesToCommit = @{ + "$env:PROFILINGINPUTSPROPSNAME" = "src/Tests/config/runsettings/Official/OptProf/External/$env:PROFILINGINPUTSPROPSNAME"; +} + +foreach ($File in $FilesToCommit.GetEnumerator()) { + $SourcePath = Join-Path $PSScriptRoot $File.Key + if (Test-Path $SourcePath) { + $DestinationPath = Join-Path (Get-Location) $File.Value + Write-Host "Copying $SourcePath to $DestinationPath" + Copy-Item -Path $SourcePath -Destination $DestinationPath + git add $DestinationPath + } + else { + Write-Host "$SourcePath is not present, skipping" + } +} diff --git a/azure-pipelines/vs-insertion.yml b/azure-pipelines/vs-insertion.yml index 6fa9f0423..0f80fd599 100644 --- a/azure-pipelines/vs-insertion.yml +++ b/azure-pipelines/vs-insertion.yml @@ -2,6 +2,11 @@ trigger: none # We only want to trigger manually or based on resources pr: none resources: + repositories: + - repository: MicroBuildTemplate + type: git + name: 1ESPipelineTemplates/MicroBuildTemplate + ref: refs/tags/release pipelines: - pipeline: CI source: vs-threading @@ -10,51 +15,86 @@ resources: trigger: tags: - Real signed - - auto-insertion -stages: -- stage: VS - displayName: VS insertion - jobs: - - deployment: insertion - pool: - vmImage: windows-latest - environment: No-Approval - strategy: - runOnce: - deploy: - steps: - - powershell: | - Write-Host "##vso[build.updatebuildnumber]$(resources.pipeline.CI.runName)" - displayName: Set pipeline name - - template: release-deployment-prep.yml - - download: CI - artifact: VSInsertion-Windows - displayName: Download VSInsertion-Windows artifact - - task: NuGetCommand@2 - displayName: Push CoreXT packages to VS feed - inputs: - command: push - packagesToPush: $(Pipeline.Workspace)/CI/VSInsertion-windows/*.nupkg - publishVstsFeed: 97a41293-2972-4f48-8c0e-05493ae82010 - allowPackageConflicts: true - - task: MicroBuildInsertVsPayload@4 - displayName: Insert VS Payload +variables: +- template: GlobalVariables.yml + +extends: + template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate + parameters: + settings: + networkIsolationPolicy: Permissive,CFSClean2 + sdl: + sourceAnalysisPool: VSEng-MicroBuildVSStable + sbom: + enabled: false + + stages: + - stage: insertion + jobs: + - job: upload + displayName: Upload VS payload + pool: VSEng-MicroBuildVSStable + templateContext: + outputParentDirectory: $(Pipeline.Workspace)/CI + steps: + - checkout: none + - powershell: Write-Host "##vso[build.updatebuildnumber]$(resources.pipeline.CI.runName)" + displayName: ⚙️ Set pipeline name + - template: azure-pipelines/release-deployment-prep.yml@self + - download: CI + artifact: VSInsertion-Windows + displayName: 🔻 Download VSInsertion-Windows artifact + - ${{ if eq(variables['ContainsVsix'], 'true') }}: + - task: 1ES.MicroBuildVstsDrop@1 + displayName: 🔺 Upload VSTS Drop inputs: - TeamName: $(TeamName) - TeamEmail: $(TeamEmail) - InsertionPayloadName: $(Build.Repository.Name) $(Build.BuildNumber) - InsertionBuildPolicy: Request Perf DDRITs - AutoCompletePR: true - AutoCompleteMergeStrategy: Squash - - task: MicroBuildCleanup@1 - displayName: Send Telemetry - - powershell: | - $contentType = 'application/json'; - $headers = @{ Authorization = 'Bearer $(System.AccessToken)' }; - $rawRequest = @{ daysValid = 365 * 2; definitionId = $(resources.pipeline.CI.pipelineID); ownerId = 'User:$(Build.RequestedForId)'; protectPipeline = $false; runId = $(resources.pipeline.CI.runId) }; - $request = ConvertTo-Json @($rawRequest); - Write-Host $request - $uri = "$(System.CollectionUri)$(System.TeamProject)/_apis/build/retention/leases?api-version=6.0-preview.1"; - Invoke-RestMethod -uri $uri -method POST -Headers $headers -ContentType $contentType -Body $request; - displayName: Retain inserted builds + dropFolder: $(Pipeline.Workspace)/CI/VSInsertion-windows/Vsix + dropName: $(VstsDropNames) + accessToken: $(System.AccessToken) + - task: 1ES.PublishNuget@1 + displayName: 📦 Push VS-repo packages to VS feed + inputs: + packagesToPush: '$(Pipeline.Workspace)/CI/VSInsertion-Windows/*.nupkg' + packageParentPath: $(Pipeline.Workspace)/CI/VSInsertion-Windows + allowPackageConflicts: true + publishVstsFeed: VS + + - job: insert + dependsOn: upload + displayName: VS insertion + pool: VSEngSS-MicroBuild2022-1ES + templateContext: + outputParentDirectory: $(Pipeline.Workspace)/CI + steps: + - checkout: none + - template: azure-pipelines/release-deployment-prep.yml@self + - download: CI + artifact: VSInsertion-Windows + displayName: 🔻 Download VSInsertion-Windows artifact + - task: MicroBuildInsertVsPayload@5 + displayName: 🏭 Insert VS Payload + inputs: + TeamName: $(TeamName) + TeamEmail: $(TeamEmail) + InsertionPayloadName: $(Build.Repository.Name) $(Build.BuildNumber) + InsertionBuildPolicies: Request Perf DDRITs + InsertionReviewers: $(Build.RequestedFor),Andrew Arnott + CustomScriptExecutionCommand: $(Pipeline.Workspace)\CI\VSInsertion-Windows\vs-insertion-script.ps1 + AutoCompletePR: true + AutoCompleteMergeStrategy: Squash + ShallowClone: true + ${{ if eq(variables['system.collectionId'], '011b8bdf-6d56-4f87-be0d-0092136884d9') }}: + ConnectedVSDropServiceName: 'VSEng-VSDrop-MI' + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + + - powershell: | + $contentType = 'application/json'; + $headers = @{ Authorization = 'Bearer $(System.AccessToken)' }; + $rawRequest = @{ daysValid = 365 * 2; definitionId = $(resources.pipeline.CI.pipelineID); ownerId = 'User:$(Build.RequestedForId)'; protectPipeline = $false; runId = $(resources.pipeline.CI.runId) }; + $request = ConvertTo-Json @($rawRequest); + Write-Host $request + $uri = "$(System.CollectionUri)$(System.TeamProject)/_apis/build/retention/leases?api-version=6.0-preview.1"; + Invoke-RestMethod -uri $uri -method POST -Headers $headers -ContentType $contentType -Body $request; + displayName: 🗻 Retain inserted builds diff --git a/azure-pipelines/vs-validation.yml b/azure-pipelines/vs-validation.yml new file mode 100644 index 000000000..180c3eeeb --- /dev/null +++ b/azure-pipelines/vs-validation.yml @@ -0,0 +1,144 @@ +# This is a top-level pipeline file, which is designed to be added as an optional PR build policy +# so that a VS insertion and all the validation that entails can be done before ever merging the PR +# in its original repo. + +trigger: none # We only want to trigger manually or based on resources +pr: none + +parameters: +- name: ShouldSkipOptimize + displayName: Skip OptProf optimization + type: boolean + default: false + +resources: + repositories: + - repository: MicroBuildTemplate + type: git + name: 1ESPipelineTemplates/MicroBuildTemplate + ref: refs/tags/release + +variables: +- template: GlobalVariables.yml +- name: MicroBuild_NuPkgSigningEnabled + value: false # test-signed nuget packages fail to restore in the VS insertion PR validations. Just don't sign them *at all*. + +extends: + template: azure-pipelines/MicroBuild.1ES.Unofficial.yml@MicroBuildTemplate + parameters: + settings: + networkIsolationPolicy: Permissive,CFSClean2 + sdl: + sourceAnalysisPool: VSEng-MicroBuildVSStable + credscan: + enabled: false + + stages: + - stage: Build + variables: + - template: /azure-pipelines/BuildStageVariables.yml@self + - name: SkipCodesignVerify + value: true + + jobs: + - template: /azure-pipelines/build.yml@self + parameters: + Is1ESPT: true + RealSign: false + ShouldSkipOptimize: ${{ parameters.ShouldSkipOptimize }} + windowsPool: VSEng-MicroBuildVSStable + linuxPool: + name: AzurePipelines-EO + demands: + - ImageOverride -equals 1ESPT-Ubuntu24.04 + os: Linux + macOSPool: + name: Azure Pipelines + vmImage: macOS-15 + os: macOS + EnableMacOSBuild: false + RunTests: false + SkipCodesignVerify: true + + - template: /azure-pipelines/prepare-insertion-stages.yml@self + parameters: + ArchiveSymbols: false + RealSign: false + + - stage: insertion + displayName: VS insertion + jobs: + - job: upload + displayName: Upload VS payload + pool: VSEng-MicroBuildVSStable + steps: + - checkout: self + clean: true + fetchDepth: 1 + - download: current + artifact: Variables-Windows + displayName: 🔻 Download Variables-Windows artifact + - powershell: $(Pipeline.Workspace)/Variables-Windows/_define.ps1 + displayName: ⚙️ Set pipeline variables based on artifacts + - download: current + artifact: VSInsertion-Windows + displayName: 🔻 Download VSInsertion-Windows artifact + - ${{ if eq(variables['ContainsVsix'], 'true') }}: + - task: 1ES.MicroBuildVstsDrop@1 + displayName: 🔺 Upload VSTS Drop + inputs: + dropFolder: $(Pipeline.Workspace)/VSInsertion-windows/Vsix + dropName: $(VstsDropNames) + accessToken: $(System.AccessToken) + - task: 1ES.PublishNuget@1 + displayName: 📦 Push VS-repo packages to VS feed + inputs: + packagesToPush: '$(Pipeline.Workspace)/VSInsertion-Windows/*.nupkg' + packageParentPath: $(Pipeline.Workspace)/VSInsertion-Windows + allowPackageConflicts: true + publishVstsFeed: VS + + - job: insertion + displayName: VS insertion + dependsOn: upload + pool: VSEngSS-MicroBuild2022-1ES + steps: + - checkout: self + clean: true + fetchDepth: 1 + - download: current + artifact: Variables-Windows + displayName: 🔻 Download Variables-Windows artifact + - powershell: $(Pipeline.Workspace)/Variables-Windows/_define.ps1 + displayName: ⚙️ Set pipeline variables based on artifacts + - download: current + artifact: VSInsertion-Windows + displayName: 🔻 Download VSInsertion-Windows artifact + - task: MicroBuildInsertVsPayload@5 + displayName: 🏭 Insert VS Payload + inputs: + TeamName: $(TeamName) + TeamEmail: $(TeamEmail) + InsertionPayloadName: $(Build.Repository.Name) VALIDATION BUILD $(Build.BuildNumber) ($(Build.SourceBranch)) [Skip-SymbolCheck] [Skip-HashCheck] [Skip-SignCheck] + InsertionDescription: | + This PR is for **validation purposes only** for !$(System.PullRequest.PullRequestId). **Do not complete**. + CustomScriptExecutionCommand: $(Pipeline.Workspace)\VSInsertion-Windows\vs-insertion-script.ps1; src\VSSDK\NuGet\AllowUnstablePackages.ps1 + InsertionBuildPolicies: Request Perf DDRITs + InsertionReviewers: $(Build.RequestedFor) + DraftPR: false # set to true and update InsertionBuildPolicy when we can specify all the validations we want to run (https://dev.azure.com/devdiv/DevDiv/_workitems/edit/2224288) + AutoCompletePR: false + ShallowClone: true + ${{ if eq(variables['system.collectionId'], '011b8bdf-6d56-4f87-be0d-0092136884d9') }}: + ConnectedVSDropServiceName: 'VSEng-VSDrop-MI' + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - powershell: | + $insertionPRId = azure-pipelines/Get-InsertionPRId.ps1 + $Markdown = @" + Validation insertion pull request created: !$insertionPRId + Please check status there before proceeding to merge this PR. + Remember to Abandon and (if allowed) to Delete Source Branch on that insertion PR when validation is complete. + "@ + azure-pipelines/PostPRMessage.ps1 -AccessToken '$(System.AccessToken)' -Markdown $Markdown -Verbose + displayName: ✏️ Comment on pull request + condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest')) diff --git a/azurepipelines-coverage.yml b/azurepipelines-coverage.yml new file mode 100644 index 000000000..e2dd1f503 --- /dev/null +++ b/azurepipelines-coverage.yml @@ -0,0 +1,6 @@ +# https://learn.microsoft.com/azure/devops/pipelines/test/codecoverage-for-pullrequests +coverage: + status: + comments: on # add comment to PRs reporting diff in coverage of modified files + diff: # diff coverage is code coverage only for the lines changed in a pull request. + target: 70% # set this to a desired %. Default is 70% diff --git a/doc/analyzers/VSTHRD001.md b/doc/analyzers/VSTHRD001.md index 5c9dbcdee..0bd13cf04 100644 --- a/doc/analyzers/VSTHRD001.md +++ b/doc/analyzers/VSTHRD001.md @@ -1,37 +1 @@ -# VSTHRD001 Avoid legacy thread switching methods - -Switching to the UI thread should be done using `JoinableTaskFactory.SwitchToMainThreadAsync` -rather than legacy methods such as `Dispatcher.Invoke` or `ThreadHelper.Invoke`. -This avoids deadlocks and can reduce threadpool starvation. - -## Examples of patterns that are flagged by this analyzer - -```csharp -void Foo() { - ThreadHelper.Generic.Invoke(delegate { - DoSomething(); - }); -} -``` - -## Solution - -Use `await SwitchToMainThreadAsync()` instead, wrapping with `JoinableTaskFactory.Run` if necessary: - -```csharp -void Foo() { - ThreadHelper.JoinableTaskFactory.Run(async delegate { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - DoSomething(); - }); -} -``` - -In the above example, we obtain a `JoinableTaskFactory` instance from the `ThreadHelper.JoinableTaskFactory` static property -as it exists within Visual Studio itself. Other applications should create and expose their own `JoinableTaskContext` and/or `JoinableTaskFactory` for use in code that run in these applications. -See our doc on [consuming `JoinableTaskFactory` from a library](https://github.com/microsoft/vs-threading/blob/main/doc/library_with_jtf.md) for more information. - -## Configuration - -This analyzer is configurable via the `vs-threading.LegacyThreadSwitchingMembers.txt` file. -See our [configuration](configuration.md) topic for more information. +This content has been moved to [GitHub Pages](https://microsoft.github.io/vs-threading/analyzers/VSTHRD001.html). diff --git a/doc/analyzers/VSTHRD002.md b/doc/analyzers/VSTHRD002.md index 6a05a94ca..69a4fc3c9 100644 --- a/doc/analyzers/VSTHRD002.md +++ b/doc/analyzers/VSTHRD002.md @@ -1,45 +1 @@ -# VSTHRD002 Avoid problematic synchronous waits - -Synchronously waiting on `Task`, `ValueTask`, or awaiters is dangerous and may cause dead locks. - -## Examples of patterns that are flagged by this analyzer - -```csharp -void DoSomething() -{ - DoSomethingElseAsync().Wait(); - DoSomethingElseAsync().GetAwaiter().GetResult(); - var result = CalculateSomethingAsync().Result; -} -``` - -## Solution - -Please consider the following options: - -1. Switch to asynchronous wait if the caller is already a "async" method. -1. Change the chain of callers to be "async" methods, and then change this code to be asynchronous await. -1. Use `JoinableTaskFactory.Run()` to wait on the tasks or awaiters. - -```csharp -async Task DoSomethingAsync() -{ - await DoSomethingElseAsync(); - await DoSomethingElseAsync(); - var result = await CalculateSomethingAsync(); -} - -void DoSomething() -{ - joinableTaskFactory.Run(async delegate - { - await DoSomethingElseAsync(); - await DoSomethingElseAsync(); - var result = await CalculateSomethingAsync(); - }); -} -``` - -Refer to [Asynchronous and multithreaded programming within VS using the JoinableTaskFactory][1] for more information. - -[1]: https://devblogs.microsoft.com/premier-developer/asynchronous-and-multithreaded-programming-within-vs-using-the-joinabletaskfactory/ +This content has been moved to [GitHub Pages](https://microsoft.github.io/vs-threading/analyzers/VSTHRD002.html). diff --git a/doc/analyzers/VSTHRD003.md b/doc/analyzers/VSTHRD003.md index 25208871f..34aba3d45 100644 --- a/doc/analyzers/VSTHRD003.md +++ b/doc/analyzers/VSTHRD003.md @@ -1,92 +1 @@ -# VSTHRD003 Avoid awaiting foreign Tasks - -Tasks that are created and run from another context (not within the currently running method or delegate) -should not be returned or awaited on. Doing so can result in deadlocks because awaiting a `Task` -does not result in the awaiter "joining" the effort such that access to the main thread is shared. -If the awaited `Task` requires the main thread, and the caller that is awaiting it is blocking the -main thread, a deadlock will result. - -When required to await a task that was started earlier, start it within a delegate passed to -`JoinableTaskFactory.RunAsync`, storing the resulting `JoinableTask` in a field or variable. -You can safely await the `JoinableTask` later. - -## Examples of patterns that are flagged by this analyzer - -The following example would likely deadlock if `MyMethod` were called on the main thread, -since `SomeOperationAsync` cannot gain access to the main thread in order to complete. - -```csharp -void MyMethod() -{ - System.Threading.Tasks.Task task = SomeOperationAsync(); - joinableTaskFactory.Run(async delegate - { - await task; /* This analyzer will report warning on this line. */ - }); -} -``` - -In the next example, `WaitForMyMethod` may deadlock when `this.task` has not completed -and needs the main thread to complete. - -```csharp -class SomeClass -{ - System.Threading.Tasks.Task task; - - SomeClass() - { - this.task = SomeOperationAsync(); - } - - async Task MyMethodAsync() - { - await this.task; /* This analyzer will report warning on this line. */ - } - - void WaitForMyMethod() - { - joinableTaskFactory.Run(() => MyMethodAsync()); - } -} -``` - -## Solution - -To await the result of an async method from with a JoinableTaskFactory.Run delegate, -invoke the async method within the JoinableTaskFactory.Run delegate: - -```csharp -void MyMethod() -{ - joinableTaskFactory.Run(async delegate - { - System.Threading.Tasks.Task task = SomeOperationAsync(); - await task; - }); -} -``` - -Alternatively wrap the original method invocation with JoinableTaskFactory.RunAsync: - -```csharp -class SomeClass -{ - JoinableTask joinableTask; - - SomeClass() - { - this.joinableTask = joinableTaskFactory.RunAsync(() => SomeOperationAsync()); - } - - async Task MyMethodAsync() - { - await this.joinableTask; - } - - void WaitForMyMethod() - { - joinableTaskFactory.Run(() => MyMethodAsync()); - } -} -``` +This content has been moved to [GitHub Pages](https://microsoft.github.io/vs-threading/analyzers/VSTHRD003.html). diff --git a/doc/analyzers/VSTHRD004.md b/doc/analyzers/VSTHRD004.md index 39d3bbdf6..8c937964f 100644 --- a/doc/analyzers/VSTHRD004.md +++ b/doc/analyzers/VSTHRD004.md @@ -1,43 +1 @@ -# VSTHRD004 Await SwitchToMainThreadAsync - -Calls to `JoinableTaskFactory.SwitchToMainThreadAsync` must be awaited -or it is a no-op. - -## Examples of patterns that are flagged by this analyzer - -```csharp -void MyMethod() -{ - joinableTaskFactory.SwitchToMainThreadAsync(); - UIThreadBoundWork(); -} -``` - -## Solution - -Add `await` in front of the call to `JoinableTaskFactory.SwitchToMainThreadAsync`. - -This requires an async context. Here, we fix the problem by making the outer method async: - -```csharp -async Task MyMethodAsync() -{ - await joinableTaskFactory.SwitchToMainThreadAsync(); - UIThreadBoundWork(); -} -``` - - -Alternatively if found in a synchronous method that cannot be made async, -this failure can be fixed by lifting the code into a delegate passed to `JoinableTaskFactory.Run`: - -```csharp -void MyMethod() -{ - joinableTaskFactory.Run(async delegate - { - await joinableTaskFactory.SwitchToMainThreadAsync(); - UIThreadBoundWork(); - }); -} -``` +This content has been moved to [GitHub Pages](https://microsoft.github.io/vs-threading/analyzers/VSTHRD004.html). diff --git a/doc/analyzers/VSTHRD010.md b/doc/analyzers/VSTHRD010.md index 022d76554..15a5d10b3 100644 --- a/doc/analyzers/VSTHRD010.md +++ b/doc/analyzers/VSTHRD010.md @@ -1,61 +1 @@ -# VSTHRD010 Invoke single-threaded types on Main thread - -Acquiring, casting, or invoking single-threaded objects should be done after ensuring -that your code is running on the main thread. - -This analyzer can be configured to: -1. Recognize the objects that are single-threaded that are unique to your app or library. -2. Recognize synchronous methods that verify the caller is already on the main thread. -3. Recognize methods that switch to the main thread when the caller awaits them. - Calls to `JoinableTaskFactory.SwitchToMainThreadAsync` methods are pre-configured. - -See our [configuration](configuration.md) topic to learn more about customizing this analyzer. - -This analyzer also recognizes requirements to use the main thread transitively within your solution. -For example, if method `A()` invokes a type that we know from configuration requires the main thread, -and `B()` calls `A()`, then the `B` method also needs the UI thread transitively. -This analyzer flags `B()` as needing to call a method that throws if not already on the main thread -only when `A()` is written to call such a method. - -**NOTE:** This analyzer requires [full solution analysis](fsa.md). - -## Examples of patterns that are flagged by this analyzer - -This example is based on the configuration available from the Visual Studio SDK -that defines `IVs*` interfaces as requiring the main thread. - -```csharp -private void CallVS() -{ - IVsSolution sln = GetIVsSolution(); - sln.SetProperty(); // This analyzer will report warning on this invocation. -} -``` - -## Solution - -First ensure you are running on the main thread before interacting with single-threaded objects. -Either throw when you are not on the appropriate thread, or explicitly switch to the -main thread. - -This solution example is based on the configuration available from the Visual Studio SDK -that defines `ThreadHelper.ThrowIfNotOnUIThread()` as one which throws if the caller -is not already on the main thread. - -```csharp -private void CallVS() -{ - ThreadHelper.ThrowIfNotOnUIThread(); - IVsSolution sln = GetIVsSolution(); - sln.SetProperty(); // This analyzer will not report warning on this invocation. -} - -private async Task CallVSAsync() -{ - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - IVsSolution sln = GetIVsSolution(); - sln.SetProperty(); // This analyzer will not report warning on this invocation. -} -``` - -Refer to [Asynchronous and multithreaded programming within VS using the JoinableTaskFactory](http://blogs.msdn.com/b/andrewarnottms/archive/2014/05/07/asynchronous-and-multithreaded-programming-within-vs-using-the-joinabletaskfactory/) for more info. +This content has been moved to [GitHub Pages](https://microsoft.github.io/vs-threading/analyzers/VSTHRD010.html). diff --git a/doc/analyzers/VSTHRD011.md b/doc/analyzers/VSTHRD011.md index 40b7e94fe..8bd47dc63 100644 --- a/doc/analyzers/VSTHRD011.md +++ b/doc/analyzers/VSTHRD011.md @@ -1,57 +1 @@ -# VSTHRD011 Use `AsyncLazy` - -The `Lazy` type executes the value factory just once and -the value factory inherits the context of the first one to request the -`Lazy.Value` property's value. This can lead to deadlocks when -the value factory attempts to switch to the main thread. - -## Examples of patterns that are flagged by this analyzer - -### Using `Lazy` where `T` is `Task` - -When `T` is `Task` (because the value factory is an async method), -if the first caller had no access to the main thread, and the value factory -requires it, it will block. If later a second caller calls the `Value` property -and that second caller is blocking the UI thread for its result, it will deadlock. - -```csharp -var lazy = new Lazy>(async delegate // analyzer flags this line -{ - await Task.Yield(); - return 3; -}); - -int value = await lazy.Value; -``` - -### Using synchronously blocking methods in `Lazy` value factories - -When the value factory passed to the `Lazy` constructor calls synchronously -blocking methods such as `JoinableTaskFactory.Run`, only the first caller -can help any required transition to the main thread. - -```csharp -var lazy = new Lazy(delegate -{ - return joinableTaskFactory.Run(async delegate { // analyzer flags this line - int result = await SomeAsyncMethod(); - return result + 3; - }); -}); - -int value = lazy.Value; -``` - -## Solution - -Use `AsyncLazy` with an async value factory: - -```csharp -var lazy = new AsyncLazy(async delegate -{ - await Task.Yield(); - return 3; -}); - -int value = await lazy.GetValueAsync(); -``` +This content has been moved to [GitHub Pages](https://microsoft.github.io/vs-threading/analyzers/VSTHRD011.html). diff --git a/doc/analyzers/VSTHRD012.md b/doc/analyzers/VSTHRD012.md index 31710a1cb..7c2578ea7 100644 --- a/doc/analyzers/VSTHRD012.md +++ b/doc/analyzers/VSTHRD012.md @@ -1,33 +1 @@ -# VSTHRD012 Provide `JoinableTaskFactory` where allowed - -When constructing types or calling methods that accept a `JoinableTaskFactory` -or `JoinableTaskContext`, take the opportunity to supply one if your application -has a main thread with a single threaded `SynchronizationContext` such as WPF or WinForms. - -## Examples of patterns that are flagged by this analyzer - -```csharp -void F() { - var o = new AsyncLazy(() => Task.FromResult(1)); // analyzer flags this line -} -``` - -## Solution - -Call the overload that accepts a `JoinableTaskFactory` or `JoinableTaskContext` instance: - -```csharp -void F() { - var o = new AsyncLazy(() => Task.FromResult(1), this.JoinableTaskFactory); -} -``` - -## Suppression - -You can suppress the diagnostic by explicitly specifying `null` for the argument: - -```csharp -void F() { - var o = new AsyncLazy(() => Task.FromResult(1), null); -} -``` +This content has been moved to [GitHub Pages](https://microsoft.github.io/vs-threading/analyzers/VSTHRD012.html). diff --git a/doc/analyzers/VSTHRD100.md b/doc/analyzers/VSTHRD100.md index 837405d0c..25d170bfe 100644 --- a/doc/analyzers/VSTHRD100.md +++ b/doc/analyzers/VSTHRD100.md @@ -1,29 +1 @@ -# VSTHRD100 Avoid `async void` methods - -Methods with `async void` signatures make it impossible for their caller to track -the entire asynchronous operation and handle exceptions that may be thrown by that method. -If the method throws an exception, it crashes the process. - -## Examples of patterns that are flagged by this analyzer - -```csharp -async void DoSomethingAsync() -{ - await SomethingElseAsync(); -} -``` - -## Solution - -Change the method to return `Task` instead of `void`. - -```csharp -async Task DoSomethingAsync() -{ - await SomethingElseAsync(); -} -``` - -A code fix is offered that automatically changes the return type of the method. - -Refer to [Async/Await - Best Practices in Asynchronous Programming](https://msdn.microsoft.com/en-us/magazine/jj991977.aspx) for more info. +This content has been moved to [GitHub Pages](https://microsoft.github.io/vs-threading/analyzers/VSTHRD100.html). diff --git a/doc/analyzers/VSTHRD101.md b/doc/analyzers/VSTHRD101.md index 10f9e411d..1c842e532 100644 --- a/doc/analyzers/VSTHRD101.md +++ b/doc/analyzers/VSTHRD101.md @@ -1,71 +1 @@ -# VSTHRD101 Avoid unsupported async delegates - -C# allows you to define async delegates or lambdas and use them in contexts that accept -void-returning delegates, thus creating an `async void` method such as is forbidden by -[VSTHRD100](VSTHRD100.md), but is much harder to catch when simply looking at the code -because for the same syntax, the C# compiler will create an `async Func` delegate -or an `async void` delegate based on the type expected by the method being invoked. - -This analyzer helps prevent inadvertent creation of `async void` delegates. - -## Examples of patterns that are flagged by this analyzer - -```csharp -void StartWatching(ObservableCollection oc) -{ - // This delegate becomes an "async void" method to match the EventHandler delegate type. - oc.CollectionChanged += async () => - { - await Task.Yield(); - }; -} - -void StartWatching(ObservableCollection oc) -{ - // This delegate becomes an "async void" method to match the Action delegate type. - Callback(async () => - { - await Task.Yield(); - }); -} - -void Callback(Action action) -{ - // out of scope of sample -} -``` - -## Solution - -1. Wrap the asynchronous behavior in another method that accepts a `Func` delegate. -1. Change the receiving method's expected delegate type to one that returns a `Task` or `Task`. -1. Implement the delegate synchronously. - -```csharp -void StartWatching(ObservableCollection oc) -{ - oc.CollectionChanged += () => - { - // The outer delegate is synchronous, but kicks off async work via a method that accepts an async delegate. - joinableTaskFactory.RunAsync(async delegate { - await Task.Yield(); - }); - }; -} - -void StartWatching(ObservableCollection oc) -{ - // This delegate becomes an "async Task" method to match the Func delegate type. - Callback(async () => - { - await Task.Yield(); - }); -} - -void Callback(Func action) -{ - // out of scope of sample -} -``` - -Refer to [Async/Await - Best Practices in Asynchronous Programming](https://msdn.microsoft.com/en-us/magazine/jj991977.aspx) for more info. +This content has been moved to [GitHub Pages](https://microsoft.github.io/vs-threading/analyzers/VSTHRD101.html). diff --git a/doc/analyzers/VSTHRD102.md b/doc/analyzers/VSTHRD102.md index 60ba1cb71..4d83049f2 100644 --- a/doc/analyzers/VSTHRD102.md +++ b/doc/analyzers/VSTHRD102.md @@ -1,56 +1 @@ -# VSTHRD102 Implement internal logic asynchronously - -Internal or private methods may be invoked by public methods that are asynchronous. -If the internal method has an opportunity to do work asynchronously, it should do so -in order that async public members can truly be async. - -## Examples of patterns that are flagged by this analyzer - -```csharp -public void PublicMethod() -{ - DoWork(); -} - -public async Task PublicMethodAsync() -{ - DoWork(); - await Task.Yield(); -} - -internal void DoWork() -{ - joinableTaskFactory.Run(async delegate // Analyzer will flag this line - { - await DoSomethingAsync(); - }); -} -``` - -Note how `DoWork()` synchronously blocks for both `PublicMethod()` and `PublicMethodAsync()`. - -## Solution - -Remove the synchronously blocking behavior and make the method async. - -```csharp -public void PublicMethod() -{ - joinableTaskFactory.Run(() => PublicMethodAsync()); -} - -public async Task PublicMethodAsync() -{ - await DoWorkAsync(); - await Task.Yield(); -} - -internal async Task DoWorkAsync() -{ - await DoSomethingAsync(); -} -``` - -Note how `DoWorkAsync()` now allows `PublicMethodAsync()` to do its work asynchronously -while `PublicMethod()` continues to synchronously block, giving your external caller the option -as to whether to do work asynchronously or synchronously. +This content has been moved to [GitHub Pages](https://microsoft.github.io/vs-threading/analyzers/VSTHRD102.html). diff --git a/doc/analyzers/VSTHRD103.md b/doc/analyzers/VSTHRD103.md index 348032057..6a94121a9 100644 --- a/doc/analyzers/VSTHRD103.md +++ b/doc/analyzers/VSTHRD103.md @@ -1,29 +1 @@ -# VSTHRD103 Call async methods when in an async method - -In a method which is already asynchronous, calls to other methods should -be to their async versions, where they exist. - -## Examples of patterns that are flagged by this analyzer - -```csharp -Task DoAsync() -{ - file.Read(buffer, 0, 10); -} -``` - -All methods where an Async-suffixed equivalent exists will produce this warning -when called from a `Task`-returning method. -In addition, calling `Task.Wait()`, `Task.Result` or `Task.GetAwaiter().GetResult()` -will produce this warning. - -## Solution - -Await the async version of the method: - -```csharp -async Task DoAsync() -{ - await file.ReadAsync(buffer, 0, 10); -} -``` +This content has been moved to [GitHub Pages](https://microsoft.github.io/vs-threading/analyzers/VSTHRD103.html). diff --git a/doc/analyzers/VSTHRD104.md b/doc/analyzers/VSTHRD104.md index aabbe5722..41e426712 100644 --- a/doc/analyzers/VSTHRD104.md +++ b/doc/analyzers/VSTHRD104.md @@ -1,36 +1 @@ -# VSTHRD104 Offer async option - -When a publicly accessible method uses `JoinableTaskFactory.Run`, there should be -another way to access the async behavior without synchronously blocking the thread -so that an async caller can be async throughout. - -This rule encourages this pattern by recognizing when some method *Foo* exists and -calls `JoinableTaskFactory.Run` that there is also a method *FooAsync*. -The recommended pattern then is for *Foo* to call *FooAsync* from the delegate -passed to `JoinableTaskFactory.Run` so that the implementation only need be written once. - -## Examples of patterns that are flagged by this analyzer - -```csharp -public void Foo() { - this.joinableTaskFactory.Run(async delegate { - await Task.Yield(); - }); -} -``` - -## Solution - -Add a FooAsync method, and (optionally) call it from the Foo method: - -```csharp -public void Foo() { - this.joinableTaskFactory.Run(async delegate { - await FooAsync(); - }); -} - -public async Task FooAsync() { - await Task.Yield(); -} -``` +This content has been moved to [GitHub Pages](https://microsoft.github.io/vs-threading/analyzers/VSTHRD104.html). diff --git a/doc/analyzers/VSTHRD105.md b/doc/analyzers/VSTHRD105.md index 06c3e34a4..62c8b7a7e 100644 --- a/doc/analyzers/VSTHRD105.md +++ b/doc/analyzers/VSTHRD105.md @@ -1,83 +1 @@ -# VSTHRD105 Avoid method overloads that assume `TaskScheduler.Current` - -Certain methods in the .NET Framework have overloads that allow specifying or omitting -a `TaskScheduler` instance. Always specify one explicitly to avoid the assumed `TaskScheduler.Current` -value, whose behavior is defined by your caller and may vary at runtime. - -The "current" `TaskScheduler` is defined by the one that is executing the currently running code. -But when your code is executing without having been scheduled by a `TaskScheduler` (as is the case with most code), -then the `TaskScheduler.Current` property returns `TaskScheduler.Default` which schedules tasks on the thread pool. -This leads many to incorrectly assume that task scheduling methods such as `StartNew` and `ContinueWith` default -to using the thread pool when in fact their default behavior varies by your caller. - -This variability in behavior leads to bugs when, for example, `TaskScheduler.Current` returns a `TaskScheduler` -that executes tasks on the application's main thread and/or only executes one task at once, such as one obtained -from the `TaskScheduler.FromCurrentSynchronizationContext()` method. -Such a circumstance often leads to deadlocks or responsiveness issues in the application. - -Always explicitly specifying `TaskScheduler.Default` (or other if appropriate) ensures your code will schedule -tasks in a predictable, consistent way. - -No diagnostic is produced by this analyzer when `TaskFactory.StartNew` is invoked on a private instance -of `TaskFactory`, since it may in fact have a safe default for `TaskScheduler`. - -Similar rules: [CA2008 (DoNotCreateTasksWithoutPassingATaskSchedulerAnalyzer)](https://github.com/dotnet/roslyn-analyzers/blob/32d8f1e397439035f0ecb5f61a9e672225f0ecdb/src/Microsoft.NetCore.Analyzers/Core/Tasks/DoNotCreateTasksWithoutPassingATaskScheduler.cs) - -## Examples of patterns that are flagged by this analyzer - -```csharp -private void FirstMethod() -{ - TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(); - Task.Factory.StartNew( - () => - { - this.AnotherMethod(); - }, - System.Threading.CancellationToken.None, - TaskCreationOptions.None, - uiScheduler); -} - -private void AnotherMethod() -{ - // TaskScheduler.Current is assumed here, which is determined by our caller. - var nestedTask = Task.Factory.StartNew( // analyzer flags this line - () => - { - // Ooops, we're still on the UI thread when called by FirstMethod. - // But we might be on the thread pool if someone else called us. - }); -} -``` - -## Solution - -Specify a `TaskScheduler` explicitly to suppress the warning: - -```csharp -private void FirstMethod() -{ - TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(); - Task.Factory.StartNew( - () => - { - this.AnotherMethod(); - }, - CancellationToken.None, - TaskCreationOptions.None, - uiScheduler); -} - -private void AnotherMethod() -{ - var nestedTask = Task.Factory.StartNew( - () => - { - // Ah, now we're reliably running on the thread pool. :) - }, - CancellationToken.None, - TaskCreationOptions.None, - TaskScheduler.Default); // Specify TaskScheduler explicitly here. -} -``` +This content has been moved to [GitHub Pages](https://microsoft.github.io/vs-threading/analyzers/VSTHRD105.html). diff --git a/doc/analyzers/VSTHRD106.md b/doc/analyzers/VSTHRD106.md index f47fc46ce..8a34ed3aa 100644 --- a/doc/analyzers/VSTHRD106.md +++ b/doc/analyzers/VSTHRD106.md @@ -1,31 +1 @@ -# VSTHRD106 Use `InvokeAsync` to raise async events - -Asynchronous events (those typed as `AsyncEventHandler`) must be raised carefully to ensure -all event handlers are invoked and awaited on. - -Although C# lets you invoke event handlers naturally, it has no awareness of async event handlers -and thus will not let you correctly await on their invocation nor invoke them sequentially. - -## Examples of patterns that are flagged by this analyzer - -```csharp -public AsyncEventHandler Clicked; - -async Task OnClicked() { - await Clicked(this, EventArgs.Empty); // only awaits the first event handler. -} -``` - -## Solution - -Use the `InvokeAsync` extension method defined in the `TplExtensions` class and await its result. -This will ensure each event handler completes before invoking the next event handler in the list, -similar to the default behavior for raising synchronous events. - -```csharp -public AsyncEventHandler Clicked; - -async Task OnClicked() { - await Clicked.InvokeAsync(this, EventArgs.Empty); // await for the completion of all handlers. -} -``` +This content has been moved to [GitHub Pages](https://microsoft.github.io/vs-threading/analyzers/VSTHRD106.html). diff --git a/doc/analyzers/VSTHRD107.md b/doc/analyzers/VSTHRD107.md index 8694cf2ea..76dab4581 100644 --- a/doc/analyzers/VSTHRD107.md +++ b/doc/analyzers/VSTHRD107.md @@ -1,27 +1 @@ -# VSTHRD107 Await Task within using expression - -The C# `using` statement requires that the used expression implement `IDisposable`. -Because `Task` implements `IDisposable`, one may accidentally omit an `await` operator -and `Dispose` of the `Task` instead of the `T` result itself when `T` derives from `IDisposable`. - -## Examples of patterns that are flagged by this analyzer - -```csharp -AsyncSemaphore lck; -using (lck.EnterAsync()) -{ - // ... -} -``` - -## Solution - -Add the `await` operator within the `using` expression. - -```csharp -AsyncSemaphore lck; -using (await lck.EnterAsync()) -{ - // ... -} -``` +This content has been moved to [GitHub Pages](https://microsoft.github.io/vs-threading/analyzers/VSTHRD107.html). diff --git a/doc/analyzers/VSTHRD108.md b/doc/analyzers/VSTHRD108.md index c2b986e2a..9771719f3 100644 --- a/doc/analyzers/VSTHRD108.md +++ b/doc/analyzers/VSTHRD108.md @@ -1,46 +1 @@ -# VSTHRD108 Assert thread affinity unconditionally - -When a method has thread affinity and throws if called from the wrong thread, it should do so without regard to any other condition. This helps ensure the caller will notice early during development that they are calling from the wrong thread. Extra conditions can hide the problem till end users discover an application failure. - -## Examples of patterns that are flagged by this analyzer - -```csharp -private int? age; - -public int GetAge() -{ - if (!this.age.HasValue) - { - ThreadHelper.ThrowIfNotOnUIThread(); - this.age = DoExpensiveUIThreadWork(); - } - - return this.age.Value; -} -``` - -The problem here is that although the UI thread is only strictly required when the field is actually initialized, callers generally cannot predict whether they will be the first or a subsequent caller. If they call from a background thread and tend to be a subsequent caller, no exception will be thrown. But under some conditions in the app when they happen to be the first caller, they'll fail at runtime because they're calling from the background thread. - -## Solution - -Move the code that throws when not on the UI thread outside the conditional block. - -```csharp -private int? age; - -public int GetAge() -{ - ThreadHelper.ThrowIfNotOnUIThread(); - if (!this.age.HasValue) - { - this.age = DoExpensiveUIThreadWork(); - } - - return this.age.Value; -} -``` - -## Configuration - -This analyzer is configurable via the `vs-threading.MainThreadAssertingMethods.txt` file. -See our [configuration](configuration.md) topic for more information. +This content has been moved to [GitHub Pages](https://microsoft.github.io/vs-threading/analyzers/VSTHRD108.html). diff --git a/doc/analyzers/VSTHRD109.md b/doc/analyzers/VSTHRD109.md index 7e28198b7..d0792db4a 100644 --- a/doc/analyzers/VSTHRD109.md +++ b/doc/analyzers/VSTHRD109.md @@ -1,29 +1 @@ -# VSTHRD109 Switch instead of assert in async methods - -Methods that are or can be async should switch to the main thread when necessary -rather than throw an exception if invoked from a different thread. -This allows callers to invoke any async method from any thread -without having to concern themselves with the threading requirements of a method that -can support its own threading requirements by switching. - -## Examples of patterns that are flagged by this analyzer - -```csharp -async Task FooAsync() { - ThreadHelper.ThrowIfNotOnUIThread(); - DoStuff(); - await DoMoreStuff(); -} -``` - -## Solution - -Use `await SwitchToMainThreadAsync()` instead: - -```csharp -async Task FooAsync() { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - DoStuff(); - await DoMoreStuff(); -} -``` +This content has been moved to [GitHub Pages](https://microsoft.github.io/vs-threading/analyzers/VSTHRD109.html). diff --git a/doc/analyzers/VSTHRD110.md b/doc/analyzers/VSTHRD110.md index 7aa764f52..42bb17ab1 100644 --- a/doc/analyzers/VSTHRD110.md +++ b/doc/analyzers/VSTHRD110.md @@ -1,75 +1 @@ -# VSTHRD110 Observe result of async calls - -Tasks returned from async methods should be awaited, or assigned to a variable for observation later. -Methods that return `Task`s often complete and report their work via the `Task` they return, and simply -invoking the method does not guarantee that its work is complete nor successful. Using the `await` keyword -just before the method call causes execution of the calling method to effectively suspend until the called -method has completed and rethrows any exception thrown by the method. - -When a `Task` or `Task` is returned and is not awaited or redirected in some other way, -within the context of a synchronous method, a warning is reported. - -This rule does *not* apply to calls made within async methods, since [CS4014][CS4014] already reports these. - -## Examples of patterns that are flagged by this analyzer - -```csharp -void Foo() { - DoStuffAsync(); -} - -async Task DoStuffAsync() { /* ... */ } -``` - -## Solution - -Convert the method to be async and await the expression: - -```csharp -async Task FooAsync() { - await DoStuffAsync(); -} - -async Task DoStuffAsync() { /* ... */ } -``` - -When the calling method's signature cannot be changed, wrap the method body in a `JoinableTaskFactory.Run` delegate instead: - -```csharp -void Foo() { - jtf.Run(async delegate { - await DoStuffAsync(); - }); -} - -async Task DoStuffAsync() { /* ... */ } -``` - -One other option is to assign the result of the method call to a field or local variable, presumably to track it later: - -```csharp -void Foo() { - Task watchThis = DoStuffAsync(); -} - -async Task DoStuffAsync() { /* ... */ } -``` - -When tracking the `Task` with a field, remember that to await it later without risk of deadlocking, -wrap it in a `JoinableTask` using `JoinableTaskFactory.RunAsync`, per [the 3rd rule](../threading_rules.md#Rule3). - -```csharp -JoinableTask watchThis; - -void Foo() { - this.watchThis = jtf.RunAsync(() => DoStuffAsync()); -} - -async Task WaitForFooToFinishAsync() { - await this.watchThis; -} - -async Task DoStuffAsync() { /* ... */ } -``` - -[CS4014]: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/cs4014 +This content has been moved to [GitHub Pages](https://microsoft.github.io/vs-threading/analyzers/VSTHRD110.html). diff --git a/doc/analyzers/VSTHRD111.md b/doc/analyzers/VSTHRD111.md index 0495e84f0..7cd14fe7a 100644 --- a/doc/analyzers/VSTHRD111.md +++ b/doc/analyzers/VSTHRD111.md @@ -1,38 +1 @@ -# VSTHRD111 Use `.ConfigureAwait(bool)` - -Some code bases, particularly libraries with no affinity to an app's UI thread, are advised to use `.ConfigureAwait(false)` for each and every _await_ because it can avoid deadlocks after those calls start on an application's UI thread and the app later decides to synchronously block the UI thread waiting for those tasks to finish. Using `.ConfigureAwait(false)` also allows continuations to switch to a background thread even when no synchronous blocking would cause a deadlock, which makes for a more responsive application and possibly higher throughput of async operations. - -Note that this scenario can also be solved using the `JoinableTaskFactory`, but many class libraries may not wish to depend on the application proffers an instance of that type to the library. Where JoinableTaskFactory _does_ apply, use of `.ConfigureAwait(false)` is _not_ recommended. See [this topic](https://github.com/Microsoft/vs-threading/blob/main/doc/cookbook_vs.md#should-i-await-a-task-with-configureawaitfalse) for more on when `.ConfigureAwait(false)` and `.ConfigureAwait(true)` are appropriate. - -**This analyzer's diagnostics are *hidden* by default**. You should enable the rule for libraries that use to require this await suffix. - -## Examples of patterns that are flagged by this analyzer - -Any await on `Task` or `ValueTask` without the `.ConfigureAwait(bool)` method called on it will be flagged. - -```csharp -async Task FooAsync() { - await DoStuffAsync(); // This line is flagged - await DoMoreStuffAsync(); // This line is flagged -} - -async Task DoStuffAsync() { /* ... */ } -async ValueTask DoMoreStuffAsync() { /* ... */ } -``` - -## Solution - -Add `.ConfigureAwait(false)` or `.ConfigureAwait(true)` to the awaited `Task` or `ValueTask`. - -```csharp -async Task FooAsync() { - await DoStuffAsync().ConfigureAwait(true); - await DoMoreStuffAsync().ConfigureAwait(false); -} - -async Task DoStuffAsync() { /* ... */ } -async ValueTask DoMoreStuffAsync() { /* ... */ } -``` - -Code fixes are offered for for this diagnostic to add either `.ConfigureAwait(false)` or `.ConfigureAwait(true)` -to an awaited expression. +This content has been moved to [GitHub Pages](https://microsoft.github.io/vs-threading/analyzers/VSTHRD111.html). diff --git a/doc/analyzers/VSTHRD112.md b/doc/analyzers/VSTHRD112.md index b9e3ae45c..edc8f6772 100644 --- a/doc/analyzers/VSTHRD112.md +++ b/doc/analyzers/VSTHRD112.md @@ -1,66 +1 @@ -# VSTHRD112 Implement `System.IAsyncDisposable` - -The `Microsoft.VisualStudio.Threading.IAsyncDisposable` interface is obsolete now that the -`System.IAsyncDisposable` interface has been defined for .NET Standard 2.0 and .NET Framework 4.6.1 -by the [`Microsoft.Bcl.AsyncInterfaces` NuGet package](https://www.nuget.org/packages/Microsoft.Bcl.AsyncInterfaces). - -New classes looking to support async disposable should use `System.IAsyncDisposable` instead of `Microsoft.VisualStudio.Threading.IAsyncDisposable`. -Existing classes that already implement `Microsoft.VisualStudio.Threading.IAsyncDisposable` should *also* implement `System.IAsyncDisposable` so the async disposal option will be recognized by code that only checks for presence of the new interface. - -## Examples of patterns that are flagged by this analyzer - -This class only implements `Microsoft.VisualStudio.Threading.IAsyncDisposable` and will produce the VSTHRD112 diagnostic: - -```cs -using Microsoft.VisualStudio.Threading; - -class SomeClass : IAsyncDisposable -{ - public Task DisposeAsync() - { - } -} -``` - -## Solution - -Implement `System.IAsyncDisposable` in addition to (or instead of) `Microsoft.VisualStudio.Threading.IAsyncDisposable`. -Add a package reference to `Microsoft.Bcl.AsyncInterfaces` if the compiler cannot find `System.IAsyncDisposable`. - -In this example, only `System.IAsyncDisposable` is supported, which is acceptable: - -```cs -using System; - -class SomeClass : IAsyncDisposable -{ - public ValueTask DisposeAsync() - { - } -} -``` - -In this next example, both interfaces are supported: - -```cs -class SomeClass : System.IAsyncDisposable, Microsoft.VisualStudio.Threading.IAsyncDisposable -{ - Task Microsoft.VisualStudio.Threading.IAsyncDisposable.DisposeAsync() - { - // Simply forward the call to the other DisposeAsync overload. - System.IAsyncDisposable self = this; - return self.DisposeAsync().AsTask(); - } - - ValueTask System.IAsyncDisposable.DisposeAsync() - { - // Interesting dispose logic here. - } -} -``` - -In the above example both `DisposeAsync` methods are explicit interface implementations. -Promoting one of the methods to be `public` is typically advised. -If one of these methods was already public and the class itself is public or protected, keep the same method public to avoid an API binary breaking change. - -An automated code fix may be offered for VSTHRD112 diagnostics. +This content has been moved to [GitHub Pages](https://microsoft.github.io/vs-threading/analyzers/VSTHRD112.html). diff --git a/doc/analyzers/VSTHRD113.md b/doc/analyzers/VSTHRD113.md index 8d4dee677..547e22219 100644 --- a/doc/analyzers/VSTHRD113.md +++ b/doc/analyzers/VSTHRD113.md @@ -1,37 +1 @@ -# VSTHRD113 Check for `System.IAsyncDisposable` - -The `Microsoft.VisualStudio.Threading.IAsyncDisposable` interface is obsolete now that the -`System.IAsyncDisposable` interface has been defined for .NET Standard 2.0 and .NET Framework 4.6.1 -by the [`Microsoft.Bcl.AsyncInterfaces` NuGet package](https://www.nuget.org/packages/Microsoft.Bcl.AsyncInterfaces). - -Existing code that tests for the `Microsoft.VisualStudio.Threading.IAsyncDisposable` interface on some object should also check for `System.IAsyncDisposable` and behave similarly in either case. -New code should consider only supporting the new `System.IAsyncDisposable` interface. - -## Examples of patterns that are flagged by this analyzer - -The following code only checks for the obsolete interface and is flagged by this diagnostic: - -```cs -using Microsoft.VisualStudio.Threading; - -if (obj is IAsyncDisposable asyncDisposable) -{ - await asyncDisposable.DisposeAsync(); -} -``` - -## Solution - -Fix this by adding a code branch for the new interface that behaves similarly -within the same containing code block: - -```cs -if (obj is Microsoft.VisualStudio.Threading.IAsyncDisposable vsThreadingAsyncDisposable) -{ - await vsThreadingAsyncDisposable.DisposeAsync(); -} -else if (obj is System.IAsyncDisposable bclAsyncDisposable) -{ - await bclAsyncDisposable.DisposeAsync(); -} -``` +This content has been moved to [GitHub Pages](https://microsoft.github.io/vs-threading/analyzers/VSTHRD113.html). diff --git a/doc/analyzers/VSTHRD114.md b/doc/analyzers/VSTHRD114.md index c20c3fb53..d7822f8ca 100644 --- a/doc/analyzers/VSTHRD114.md +++ b/doc/analyzers/VSTHRD114.md @@ -1,31 +1 @@ -# VSTHRD114 Avoid returning a null Task - -Returning `null` from a non-async `Task`/`Task` method will cause a `NullReferenceException` at runtime. This problem can be avoided by returning `Task.CompletedTask`, `Task.FromResult(null)` or `Task.FromResult(default(T))` instead. - -## Examples of patterns that are flagged by this analyzer - -Any non-async `Task` returning method with an explicit `return null;` will be flagged. - -```csharp -Task DoAsync() { - return null; -} - -Task GetSomethingAsync() { - return null; -} -``` - -## Solution - -Return a task like `Task.CompletedTask` or `Task.FromResult`. - -```csharp -Task DoAsync() { - return Task.CompletedTask; -} - -Task GetSomethingAsync() { - return Task.FromResult(null); -} -``` +This content has been moved to [GitHub Pages](https://microsoft.github.io/vs-threading/analyzers/VSTHRD114.html). diff --git a/doc/analyzers/VSTHRD115.md b/doc/analyzers/VSTHRD115.md new file mode 100644 index 000000000..419b7bf3c --- /dev/null +++ b/doc/analyzers/VSTHRD115.md @@ -0,0 +1 @@ +This content has been moved to [GitHub Pages](https://microsoft.github.io/vs-threading/analyzers/VSTHRD115.html). diff --git a/doc/analyzers/VSTHRD200.md b/doc/analyzers/VSTHRD200.md index 3f87cf273..abe7406d3 100644 --- a/doc/analyzers/VSTHRD200.md +++ b/doc/analyzers/VSTHRD200.md @@ -1,47 +1 @@ -# VSTHRD200 Use `Async` suffix for async methods - -The .NET Guidelines for async methods includes that such methods -should have names that include an "Async" suffix. - -Methods that return awaitable types such as `Task` or `ValueTask` -should have an Async suffix. -Methods that do not return awaitable types should not use the Async suffix. - -## Examples of patterns that are flagged by this analyzer - -This `Task`-returning method should have a name that ends with Async: - -```csharp -async Task DoSomething() // analyzer flags this line -{ - await Task.Yield(); -} -``` - -This method should not have a name that ends with Async, since it does not return an awaitable type: - -```csharp -bool DoSomethingElseAsync() // analyzer flags this line -{ - return false; -} -``` - -## Solution - -Simply rename the method to end in "Async" (or remove the suffix, as appropriate): - -```csharp -async Task DoSomethingAsync() -{ - await Task.Yield(); -} - -bool DoSomethingElse() -{ - return false; -} -``` - - -A code fix exists to automatically rename such methods. +This content has been moved to [GitHub Pages](https://microsoft.github.io/vs-threading/analyzers/VSTHRD200.html). diff --git a/doc/editorconfigs/AppWithMainThread.editorconfig b/doc/editorconfigs/AppWithMainThread.editorconfig new file mode 100644 index 000000000..41ad1294f --- /dev/null +++ b/doc/editorconfigs/AppWithMainThread.editorconfig @@ -0,0 +1,4 @@ +# This file applies to applications with a main thread, and libraries that may run in them. + +# This file is intentionally empty because the default severity levels for the threading analyzers +# are optimized for these types of projects. diff --git a/doc/editorconfigs/AppWithoutMainThread.editorconfig b/doc/editorconfigs/AppWithoutMainThread.editorconfig new file mode 100644 index 000000000..960bfc41f --- /dev/null +++ b/doc/editorconfigs/AppWithoutMainThread.editorconfig @@ -0,0 +1,11 @@ +# These settings are appropriate for most projects that *never* run inside a process with a main thread and `SynchronizationContext`. +# Examples of such applications are ASP.NET Core and console applications. + +# VSTHRD012: Provide JoinableTaskFactory where allowed +dotnet_diagnostic.VSTHRD012.severity = none + +# VSTHRD003: Avoid awaiting foreign Tasks +dotnet_diagnostic.VSTHRD003.severity = none + +# VSTHRD010: Invoke single-threaded types on Main thread +dotnet_diagnostic.VSTHRD010.severity = none diff --git a/doc/editorconfigs/JTFFocusedLibrary.editorconfig b/doc/editorconfigs/JTFFocusedLibrary.editorconfig new file mode 100644 index 000000000..9aef6834c --- /dev/null +++ b/doc/editorconfigs/JTFFocusedLibrary.editorconfig @@ -0,0 +1,4 @@ +# These settings are appropriate for libraries that always run within a process that follows the JoinableTaskFactory rules. + +# VSTHRD111: Use .ConfigureAwait(bool) +dotnet_diagnostic.VSTHRD111.severity = silent diff --git a/doc/editorconfigs/README.md b/doc/editorconfigs/README.md new file mode 100644 index 000000000..d07b6ff8c --- /dev/null +++ b/doc/editorconfigs/README.md @@ -0,0 +1,53 @@ +# About these .editorconfig files + +This folder contains sample .editorconfig files applicable to various project types. + +Choose the most applicable .editorconfig file based on your project type and its filename and the introductory comments that may be included as a header in each file. +Append the contents of that file to your own `.editorconfig` file in your repo. + +## Use of `warning` severity levels + +When the analyzers use the `warning` severity level by default, or when the `.editorconfig` files in this folder set them, it is with the expectation that compilation warnings cause build breaks in PR/CI builds. +Using `warning` allows for a faster inner dev-loop because certain threading violations are permissible while drafting code changes, but _should_ be fixed before code is merged into the main branch. + +You can configure your CI/PR build to fail on compilation warnings by setting the `MSBuildTreatWarningsAsErrors` environment or pipeline variable to `true`. + +If your repo does _not_ have builds configured to fail on compilation warnings, consider elevating all the warning severites to error severities to ensure these serious issues do not get ignored. + +## More about specific project types + +While several project types have specific .editorconfig files defined in this folder, some merit some additional explanation and guidance. + +### Broadly shared libraries (non-Visual Studio specific) + +[SharedLibrary.editorconfig](SharedLibrary.editorconfig) + +Libraries that may run in any process, whether they have a main thread or not, should code themselves defensively to avoid any dependency on the main thread so that applications that do not follow `JoinableTaskFactory` rules can avoid deadlocks even when synchronously blocking their main thread using `Task.Wait()` on code running inside your library. +In particular, shared libraries of general interest should _always_ use `.ConfigureAwait(false)` when awaiting on tasks. + +[Learn more about authoring libraries following best threading practices](https://microsoft.github.io/vs-threading/docs/library_with_jtf.html). + +### Libraries that run inside a JoinableTaskFactory-compliant application + +[JTFFocusedLibrary.editorconfig](JTFFocusedLibrary.editorconfig) + +These are libraries that always run within a process that follows the JoinableTaskFactory rules, such as the Visual Studio process. +Because these processes _may_ block the main thread using `JoinableTaskFactor.Run` or similar APIs, the most efficient thing for a library to do is _not_ use `.ConfigureAwait(false)` everywhere so that its continuations may resume on the thread that is already blocking on its completion. + +### GUI applications and libraries specific to them + +[AppWithMainThread.editorconfig](AppWithMainThread.editorconfig) + +This essentially captures all projects that are designed specifically to run inside an application that uses a `SynchronizationContext` on its main thread to keep code on the main thread, such as WinForms, WPF, Maui and Avalonia. + +These projects are strongly encouraged to include the `Microsoft.VisualStudio.Threading` NuGet package as a dependency and the analyzer modifications below are consistent with that recommendation. + +### ASP.NET Core and console applications + +[AppWithoutMainThread.editorconfig](AppWithoutMainThread.editorconfig) + +### Test projects + +[Tests.editorconfig](Tests.editorconfig) + +Test projects have a high tendency to define async test methods that are only called by reflection, and the `Async` method name suffix is usually unwelcome there. diff --git a/doc/editorconfigs/SharedLibrary.editorconfig b/doc/editorconfigs/SharedLibrary.editorconfig new file mode 100644 index 000000000..3128e3141 --- /dev/null +++ b/doc/editorconfigs/SharedLibrary.editorconfig @@ -0,0 +1,2 @@ +# VSTHRD111: Use .ConfigureAwait(bool) +dotnet_diagnostic.VSTHRD111.severity = warning diff --git a/doc/editorconfigs/Tests.editorconfig b/doc/editorconfigs/Tests.editorconfig new file mode 100644 index 000000000..5b854513b --- /dev/null +++ b/doc/editorconfigs/Tests.editorconfig @@ -0,0 +1,4 @@ +# These settings are appropriate for test projects. + +# VSTHRD200: Use `Async` naming convention +dotnet_diagnostic.VSTHRD200.severity = none diff --git a/doc/index.md b/doc/index.md deleted file mode 100644 index 7c65607ed..000000000 --- a/doc/index.md +++ /dev/null @@ -1,14 +0,0 @@ -# Threading documentation - -## Overview - -* [3 Threading Rules](threading_rules.md) -* [Diagnostic analyzer rules](analyzers/index.md) -* [Cookbook for Visual Studio](cookbook_vs.md) -* [Testing a Visual Studio extension that uses JoinableTaskFactory](testing_vs.md) -* [Authoring a library with a JoinableTaskFactory dependency](library_with_jtf.md) - -## Performance and responsiveness investigation techniques - -* [Async hang investigations](async_hang.md) -* [Investigating Threadpool starvation issues](threadpool_starvation.md) diff --git a/docfx/.gitignore b/docfx/.gitignore new file mode 100644 index 000000000..d5bcab175 --- /dev/null +++ b/docfx/.gitignore @@ -0,0 +1,2 @@ +_site/ +api/ diff --git a/docfx/analyzers/VSTHRD001.md b/docfx/analyzers/VSTHRD001.md new file mode 100644 index 000000000..1dd47c1ac --- /dev/null +++ b/docfx/analyzers/VSTHRD001.md @@ -0,0 +1,82 @@ +# VSTHRD001 Avoid legacy thread switching methods + +Switching to the UI thread should be done using `JoinableTaskFactory.SwitchToMainThreadAsync` +rather than legacy methods such as `Dispatcher.Invoke` or `ThreadHelper.Invoke`. +This avoids deadlocks and can reduce threadpool starvation. + +## Examples of patterns that are flagged by this analyzer + +```csharp +ThreadHelper.Generic.Invoke(delegate { + DoSomething(); +}); +``` + +or + +```cs +Dispatcher.CurrentDispatcher.BeginInvoke(delegate { + DoSomething(); +}); +``` + +## Solution + +Use `await SwitchToMainThreadAsync()` instead, wrapping with the `JoinableTaskFactory`'s `Run` or `RunAsync` method if necessary: + +```csharp +void Foo() { + ThreadHelper.JoinableTaskFactory.Run(async delegate { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + DoSomething(); + }); +} +``` + +In the above example, we obtain a `JoinableTaskFactory` instance from the `ThreadHelper.JoinableTaskFactory` static property +as it exists within Visual Studio itself. Other applications should create and expose their own `JoinableTaskContext` and/or `JoinableTaskFactory` for use in code that run in these applications. +See our doc on [consuming `JoinableTaskFactory` from a library](https://github.com/microsoft/vs-threading/blob/main/doc/library_with_jtf.md) for more information. + +### Replacing Dispatcher.BeginInvoke + +When updating calls to `Dispatcher.BeginInvoke`, there are a few considerations to consider. + +1. `BeginInvoke` schedules the delegate for execution later. +1. `BeginInvoke` always executes the delegate on the dispatcher's thread. +1. `BeginInvoke` schedules the delegate at some given priority, or default priority determined by the dispatcher. + +To resolve a warning for such code, it is often sufficient to replace it with this, which is *roughly* equivalent: + +```cs +await joinableTaskFactory.RunAsync(async delegate { + await joinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true); + DoSomething(); +}) +``` + +The first line in the delegate is necessary to match the behaviors of 1 and 2 on the above list. +When the caller is known to already be on the main thread, you can simplify it slightly to this: + +```cs +await joinableTaskFactory.RunAsync(async delegate { + await Task.Yield(); + DoSomething(); +}) +``` + +Matching behavior 3 on the list above may be important when the dispatcher priority is specified in the BeginInvoke call and was chosen for a particular reason. +In such a case, you can ensure that `JoinableTaskFactory` matches that priority instead of using its default by creating a special `JoinableTaskFactory` instance with the priority setting you require using the [`JoinableTaskFactory.WithPriority`](https://learn.microsoft.com/dotnet/api/microsoft.visualstudio.threading.dispatcherextensions.withpriority?view=visualstudiosdk-2022) method. + +Altogether, this might look like: + +```cs +await joinableTaskFactory.WithPriority(DispatcherPriority.DataBind).RunAsync(async delegate { + await joinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true); + DoSomething(); +}) +``` + +## Configuration + +This analyzer is configurable via the `vs-threading.LegacyThreadSwitchingMembers.txt` file. +See our [configuration](configuration.md) topic for more information. diff --git a/docfx/analyzers/VSTHRD002.md b/docfx/analyzers/VSTHRD002.md new file mode 100644 index 000000000..6a05a94ca --- /dev/null +++ b/docfx/analyzers/VSTHRD002.md @@ -0,0 +1,45 @@ +# VSTHRD002 Avoid problematic synchronous waits + +Synchronously waiting on `Task`, `ValueTask`, or awaiters is dangerous and may cause dead locks. + +## Examples of patterns that are flagged by this analyzer + +```csharp +void DoSomething() +{ + DoSomethingElseAsync().Wait(); + DoSomethingElseAsync().GetAwaiter().GetResult(); + var result = CalculateSomethingAsync().Result; +} +``` + +## Solution + +Please consider the following options: + +1. Switch to asynchronous wait if the caller is already a "async" method. +1. Change the chain of callers to be "async" methods, and then change this code to be asynchronous await. +1. Use `JoinableTaskFactory.Run()` to wait on the tasks or awaiters. + +```csharp +async Task DoSomethingAsync() +{ + await DoSomethingElseAsync(); + await DoSomethingElseAsync(); + var result = await CalculateSomethingAsync(); +} + +void DoSomething() +{ + joinableTaskFactory.Run(async delegate + { + await DoSomethingElseAsync(); + await DoSomethingElseAsync(); + var result = await CalculateSomethingAsync(); + }); +} +``` + +Refer to [Asynchronous and multithreaded programming within VS using the JoinableTaskFactory][1] for more information. + +[1]: https://devblogs.microsoft.com/premier-developer/asynchronous-and-multithreaded-programming-within-vs-using-the-joinabletaskfactory/ diff --git a/docfx/analyzers/VSTHRD003.md b/docfx/analyzers/VSTHRD003.md new file mode 100644 index 000000000..49d8ad87b --- /dev/null +++ b/docfx/analyzers/VSTHRD003.md @@ -0,0 +1,267 @@ +# VSTHRD003 Avoid awaiting foreign Tasks + +Tasks that are created and run from another context (not within the currently running method or delegate) +should not be returned or awaited on. Doing so can result in deadlocks because awaiting a `Task` +does not result in the awaiter "joining" the effort such that access to the main thread is shared. +If the awaited `Task` requires the main thread, and the caller that is awaiting it is blocking the +main thread, a deadlock will result. + +When required to await a task that was started earlier, start it within a delegate passed to +`JoinableTaskFactory.RunAsync`, storing the resulting `JoinableTask` in a field or variable. +You can safely await the `JoinableTask` later. + +## Simple examples of patterns that are flagged by this analyzer + +The following example would likely deadlock if `MyMethod` were called on the main thread, +since `SomeOperationAsync` cannot gain access to the main thread in order to complete. + +```csharp +void MyMethod() +{ + System.Threading.Tasks.Task task = SomeOperationAsync(); + joinableTaskFactory.Run(async delegate + { + await task; /* This analyzer will report warning on this line. */ + }); +} +``` + +In the next example, `WaitForMyMethod` may deadlock when `this.task` has not completed +and needs the main thread to complete. + +```csharp +class SomeClass +{ + System.Threading.Tasks.Task task; + + SomeClass() + { + this.task = SomeOperationAsync(); + } + + async Task MyMethodAsync() + { + await this.task; /* This analyzer will report warning on this line. */ + } + + void WaitForMyMethod() + { + joinableTaskFactory.Run(() => MyMethodAsync()); + } +} +``` + +More [advanced examples](#advanced-cases) are further down in this document, below the solution section for the simpler examples. + +## Solution for simpler cases + +To await the result of an async method from with a JoinableTaskFactory.Run delegate, +invoke the async method within the JoinableTaskFactory.Run delegate: + +```csharp +void MyMethod() +{ + joinableTaskFactory.Run(async delegate + { + System.Threading.Tasks.Task task = SomeOperationAsync(); + await task; + }); +} +``` + +Alternatively wrap the original method invocation with JoinableTaskFactory.RunAsync: + +```csharp +class SomeClass +{ + JoinableTask joinableTask; + + SomeClass() + { + this.joinableTask = joinableTaskFactory.RunAsync(() => SomeOperationAsync()); + } + + async Task MyMethodAsync() + { + await this.joinableTask; + } + + void WaitForMyMethod() + { + joinableTaskFactory.Run(() => MyMethodAsync()); + } +} +``` + +## Advanced cases + +### `TaskCompletionSource` + +In the next example, a `TaskCompletionSource` is used as a black-box for unblocking functionality. +It too represents awaiting a foreign task: + +```cs +class SomeClass +{ + TaskCompletionSource tcs = new(); + + public async Task MyMethodAsync() + { + await this.tcs.Task; /* This analyzer will report warning on this line. */ + /* do more stuff */ + } + + void UnlockProgress() + { + this.tcs.TrySetResult(true); + } +} +``` + +The problem with the above code is that `MyMethodAsync()` waits for unknown work (whatever work will lead to the completion of the `TaskCompletionSource`) before making progress. +If `UnlockProgress()` is never called, the caller of `MyMethodAsync()` will be awaiting forever. +Now suppose that the caller of `MyMethodAsync()` is actually inside a `JoinableTaskFactory.Run` delegate: + +```cs +void SomeCaller() +{ + joinableTaskFactory.Run(async delegate + { + await someClass.MyMethodAsync(); + }); +} +``` + +If `SomeCaller()` runs on the main thread, then it will effectively block the main thread while waiting for `this.tcs.Task` from `SomeClass` to complete. +Now suppose that another thread comes along and wants to do some work before calling `UnlockProgress()`: + +```cs +partial class SomeClass +{ + async Task KeyMasterAsync() + { + await joinableTaskFactory.SwitchToMainThreadAsync(); + // do some work + // Unblock others + someClass.UnlockProgress(); + } +} +``` + +We have a deadlock, because `SomeCaller()` is blocking the main thread while waiting for `UnlockProgress()` to be called, but `UnlockProgress()` will not be called until `KeyMasterAsync` can reach the main thread. + +Fixing this fundamentally means that `SomeCaller` will need to *join* whatever work may be needed to ultimately call `UnlockProgress`. But for `SomeCaller`, that work is unknown, since it's at least partially inside another class. +`TaskCompletionSource` is fundamentally a blackbox and the most difficult thing to use correctly while avoiding deadlocks. + +Preferred solutions involve replacing `TaskCompletionSource` with another type that makes tracking the work involved automatic. +These include: + +1. Use `JoinableTaskFactory.RunAsync` and store the resulting `JoinableTask` in a field to await later. +1. Use `AsyncLazy` for one-time init work that should only start if required. Be sure to pass in a `JoinableTaskFactory` instance to its constructor. + +Assuming you must keep using `TaskCompletionSource` though, here's how it can be done as safely as possible. +Joining a set of unknown work is best done with the `JoinableTaskCollection` class. +It is the responsibility of `SomeClass` in the example above to work with this collection to avoid deadlocks, like this: + +```cs +class SomeClass +{ + TaskCompletionSource tcs = new(); + JoinableTaskCollection jtc; + JoinableTaskFactory jtf; + + internal SomeClass(JoinableTaskContext joinableTaskContext) + { + this.jtc = joinableTaskContext.CreateCollection(); + this.jtf = joinableTaskContext.CreateFactory(this.jtc); + } + + public async Task MyMethodAsync() + { + // Our caller is interested in completion of the TaskCompletionSource, + // so join the collected effort while waiting, to avoid deadlocks. + using (this.jtc.Join()) + { + await this.tcs.Task; /* This analyzer will report warning on this line. */ + } + + /* do more stuff */ + } + + void UnlockProgress() + { + this.tcs.TrySetResult(true); + } + + async Task KeyMasterAsync() + { + // As this method must complete to signal the TaskCompletionSource, + // all of its work must be done within the context of a JoinableTask + // that belongs to the JoinableTaskCollection. + // jtf.RunAsync will add the JoinableTask it creates to the jtc collection + // because jtf was created with jtc as an argument in our constructor. + await this.jtf.RunAsync(async delegate + { + // Because we're in the jtc collection, anyone waiting on MyMethodAsync + // will automatically lend us use of the main thread if they have it + // to avoid deadlocks. + // It does NOT matter whether we use jtf or another JoinableTaskFactory instance + // at this point. + await anyOldJTF.SwitchToMainThreadAsync(); + + // do some work + // Unblock others + this.UnlockProgress(); + }); + } +} +``` + +Notice how the public API of the class does not need to expose any `JoinableTask`-related types. +It's an implementation detail of the class. + +This works fine when the class itself fully controls the work to complete the `TaskCompletionSource`. +When _other_ classes also do work (independently of work started within `SomeClass`), the placement and access to the `JoinableTaskFactory` that is associated with the `JoinableTaskCollection` may need to be elevated so that other classes can access it as well so that *all* the work required to complete the `TaskCompletionSource` will be tracked. + +### Task chaining or other means to ensure sequential execution + +Task chaining is another technique that can lead to deadlocks. +Task chaining is where a single `Task` is kept in a field and used to call `Task.ContinueWith` to append another Task, and the resulting Task is then assigned to the field, like this: + +```cs +class TaskChainingExample +{ + private readonly object lockObject = new(); + private Task lastTask = Task.CompletedTask; + + internal Task AddWorkToEnd(Funk work) + { + lock (this.lockObject) + { + return this.lastTask = this.lastTask.ContinueWith(_ => work()).Unwrap(); + } + } +} +``` + +(Note: The above example has several *other* issues that would require more code to address, but it illustrates the idea of task chaining.) + +The deadlock risk with task chaining is that again, the chain of tasks come together to form a kind of private queue which the `JoinableTaskFactory` has no visibility into. +When a task is not at the front of the queue but its owner blocks the main thread for its completion, and if any other task ahead of it in the queue needs the main thread, a deadlock will result. + +For this reason (and several others), task chaining is *not* recommended. +Instead, you can achieve a thread-safe queue that executes work sequentially by utilizing the `ReentrantSemaphore` class. + +Fixing the above example would translate to this (allowing for a variety of reentrancy modes): + +```cs +class SequentialExecutingQueueExample +{ + private readonly ReentrantSemaphore semaphore = ReentrantSemaphore.Create(initialCount: 1, joinableTaskContext, ReentrancyMode.Stack); + + internal Task AddWorkToEnd(Func work) + { + return semaphore.ExecuteAsync(work); + } +} +``` diff --git a/docfx/analyzers/VSTHRD004.md b/docfx/analyzers/VSTHRD004.md new file mode 100644 index 000000000..39d3bbdf6 --- /dev/null +++ b/docfx/analyzers/VSTHRD004.md @@ -0,0 +1,43 @@ +# VSTHRD004 Await SwitchToMainThreadAsync + +Calls to `JoinableTaskFactory.SwitchToMainThreadAsync` must be awaited +or it is a no-op. + +## Examples of patterns that are flagged by this analyzer + +```csharp +void MyMethod() +{ + joinableTaskFactory.SwitchToMainThreadAsync(); + UIThreadBoundWork(); +} +``` + +## Solution + +Add `await` in front of the call to `JoinableTaskFactory.SwitchToMainThreadAsync`. + +This requires an async context. Here, we fix the problem by making the outer method async: + +```csharp +async Task MyMethodAsync() +{ + await joinableTaskFactory.SwitchToMainThreadAsync(); + UIThreadBoundWork(); +} +``` + + +Alternatively if found in a synchronous method that cannot be made async, +this failure can be fixed by lifting the code into a delegate passed to `JoinableTaskFactory.Run`: + +```csharp +void MyMethod() +{ + joinableTaskFactory.Run(async delegate + { + await joinableTaskFactory.SwitchToMainThreadAsync(); + UIThreadBoundWork(); + }); +} +``` diff --git a/docfx/analyzers/VSTHRD010.md b/docfx/analyzers/VSTHRD010.md new file mode 100644 index 000000000..bfb01c11d --- /dev/null +++ b/docfx/analyzers/VSTHRD010.md @@ -0,0 +1,61 @@ +# VSTHRD010 Invoke single-threaded types on Main thread + +Acquiring, casting, or invoking single-threaded objects should be done after ensuring +that your code is running on the main thread. + +This analyzer can be configured to: +1. Recognize the objects that are single-threaded that are unique to your app or library. +2. Recognize synchronous methods that verify the caller is already on the main thread. +3. Recognize methods that switch to the main thread when the caller awaits them. + Calls to `JoinableTaskFactory.SwitchToMainThreadAsync` methods are pre-configured. + +See our [configuration](configuration.md) topic to learn more about customizing this analyzer. + +This analyzer also recognizes requirements to use the main thread transitively within your solution. +For example, if method `A()` invokes a type that we know from configuration requires the main thread, +and `B()` calls `A()`, then the `B` method also needs the UI thread transitively. +This analyzer flags `B()` as needing to call a method that throws if not already on the main thread +only when `A()` is written to call such a method. + +**NOTE:** This analyzer requires [full solution analysis](fsa.md). + +## Examples of patterns that are flagged by this analyzer + +This example is based on the configuration available from the Visual Studio SDK +that defines `IVs*` interfaces as requiring the main thread. + +```csharp +private void CallVS() +{ + IVsSolution sln = GetIVsSolution(); + sln.SetProperty(); // This analyzer will report warning on this invocation. +} +``` + +## Solution + +First ensure you are running on the main thread before interacting with single-threaded objects. +Either throw when you are not on the appropriate thread, or explicitly switch to the +main thread. + +This solution example is based on the configuration available from the Visual Studio SDK +that defines `ThreadHelper.ThrowIfNotOnUIThread()` as one which throws if the caller +is not already on the main thread. + +```csharp +private void CallVS() +{ + ThreadHelper.ThrowIfNotOnUIThread(); + IVsSolution sln = GetIVsSolution(); + sln.SetProperty(); // This analyzer will not report warning on this invocation. +} + +private async Task CallVSAsync() +{ + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + IVsSolution sln = GetIVsSolution(); + sln.SetProperty(); // This analyzer will not report warning on this invocation. +} +``` + +Refer to [Asynchronous and multithreaded programming within VS using the JoinableTaskFactory](https://devblogs.microsoft.com/premier-developer/asynchronous-and-multithreaded-programming-within-vs-using-the-joinabletaskfactory/) for more info. diff --git a/docfx/analyzers/VSTHRD011.md b/docfx/analyzers/VSTHRD011.md new file mode 100644 index 000000000..40b7e94fe --- /dev/null +++ b/docfx/analyzers/VSTHRD011.md @@ -0,0 +1,57 @@ +# VSTHRD011 Use `AsyncLazy` + +The `Lazy` type executes the value factory just once and +the value factory inherits the context of the first one to request the +`Lazy.Value` property's value. This can lead to deadlocks when +the value factory attempts to switch to the main thread. + +## Examples of patterns that are flagged by this analyzer + +### Using `Lazy` where `T` is `Task` + +When `T` is `Task` (because the value factory is an async method), +if the first caller had no access to the main thread, and the value factory +requires it, it will block. If later a second caller calls the `Value` property +and that second caller is blocking the UI thread for its result, it will deadlock. + +```csharp +var lazy = new Lazy>(async delegate // analyzer flags this line +{ + await Task.Yield(); + return 3; +}); + +int value = await lazy.Value; +``` + +### Using synchronously blocking methods in `Lazy` value factories + +When the value factory passed to the `Lazy` constructor calls synchronously +blocking methods such as `JoinableTaskFactory.Run`, only the first caller +can help any required transition to the main thread. + +```csharp +var lazy = new Lazy(delegate +{ + return joinableTaskFactory.Run(async delegate { // analyzer flags this line + int result = await SomeAsyncMethod(); + return result + 3; + }); +}); + +int value = lazy.Value; +``` + +## Solution + +Use `AsyncLazy` with an async value factory: + +```csharp +var lazy = new AsyncLazy(async delegate +{ + await Task.Yield(); + return 3; +}); + +int value = await lazy.GetValueAsync(); +``` diff --git a/docfx/analyzers/VSTHRD012.md b/docfx/analyzers/VSTHRD012.md new file mode 100644 index 000000000..31710a1cb --- /dev/null +++ b/docfx/analyzers/VSTHRD012.md @@ -0,0 +1,33 @@ +# VSTHRD012 Provide `JoinableTaskFactory` where allowed + +When constructing types or calling methods that accept a `JoinableTaskFactory` +or `JoinableTaskContext`, take the opportunity to supply one if your application +has a main thread with a single threaded `SynchronizationContext` such as WPF or WinForms. + +## Examples of patterns that are flagged by this analyzer + +```csharp +void F() { + var o = new AsyncLazy(() => Task.FromResult(1)); // analyzer flags this line +} +``` + +## Solution + +Call the overload that accepts a `JoinableTaskFactory` or `JoinableTaskContext` instance: + +```csharp +void F() { + var o = new AsyncLazy(() => Task.FromResult(1), this.JoinableTaskFactory); +} +``` + +## Suppression + +You can suppress the diagnostic by explicitly specifying `null` for the argument: + +```csharp +void F() { + var o = new AsyncLazy(() => Task.FromResult(1), null); +} +``` diff --git a/docfx/analyzers/VSTHRD100.md b/docfx/analyzers/VSTHRD100.md new file mode 100644 index 000000000..b1a7278d5 --- /dev/null +++ b/docfx/analyzers/VSTHRD100.md @@ -0,0 +1,56 @@ +# VSTHRD100 Avoid `async void` methods + +Methods with `async void` signatures make it impossible for their caller to track +the entire asynchronous operation and handle exceptions that may be thrown by that method. +If the method throws an exception, it crashes the process. + +## Examples of patterns that are flagged by this analyzer + +```csharp +async void DoSomethingAsync() +{ + await SomethingElseAsync(); +} +``` + +## Solution + +Change the method to return `Task` instead of `void`. + +```csharp +async Task DoSomethingAsync() +{ + await SomethingElseAsync(); +} +``` + +A code fix is offered that automatically changes the return type of the method. + +### Event handlers + +For event handlers, avoid `async void` by using `RunAsync`: +```csharp +obj.Event += (s, e) => joinableTaskFactory.RunAsync(() => OnEventAsync(s, e)); +} + +private async Task OnEventAsync(object sender, EventArgs e) +{ + // async code here. +} +``` + +When using method group syntax as an argument, you can define the method with the required signature, without the `async` modifier, and define an anonymous delegate or lambda within the method, like this: + +```cs +var menuItem = new MenuCommand(HandleEvent, commandId); + +private void HandleEvent(object sender, EventArgs e) +{ + _ = joinableTaskFactory.RunAsync(async () => + { + // async code + }); +} +``` + +Refer to [Async/Await - Best Practices in Asynchronous Programming](https://msdn.microsoft.com/en-us/magazine/jj991977.aspx) for more info. diff --git a/docfx/analyzers/VSTHRD101.md b/docfx/analyzers/VSTHRD101.md new file mode 100644 index 000000000..10f9e411d --- /dev/null +++ b/docfx/analyzers/VSTHRD101.md @@ -0,0 +1,71 @@ +# VSTHRD101 Avoid unsupported async delegates + +C# allows you to define async delegates or lambdas and use them in contexts that accept +void-returning delegates, thus creating an `async void` method such as is forbidden by +[VSTHRD100](VSTHRD100.md), but is much harder to catch when simply looking at the code +because for the same syntax, the C# compiler will create an `async Func` delegate +or an `async void` delegate based on the type expected by the method being invoked. + +This analyzer helps prevent inadvertent creation of `async void` delegates. + +## Examples of patterns that are flagged by this analyzer + +```csharp +void StartWatching(ObservableCollection oc) +{ + // This delegate becomes an "async void" method to match the EventHandler delegate type. + oc.CollectionChanged += async () => + { + await Task.Yield(); + }; +} + +void StartWatching(ObservableCollection oc) +{ + // This delegate becomes an "async void" method to match the Action delegate type. + Callback(async () => + { + await Task.Yield(); + }); +} + +void Callback(Action action) +{ + // out of scope of sample +} +``` + +## Solution + +1. Wrap the asynchronous behavior in another method that accepts a `Func` delegate. +1. Change the receiving method's expected delegate type to one that returns a `Task` or `Task`. +1. Implement the delegate synchronously. + +```csharp +void StartWatching(ObservableCollection oc) +{ + oc.CollectionChanged += () => + { + // The outer delegate is synchronous, but kicks off async work via a method that accepts an async delegate. + joinableTaskFactory.RunAsync(async delegate { + await Task.Yield(); + }); + }; +} + +void StartWatching(ObservableCollection oc) +{ + // This delegate becomes an "async Task" method to match the Func delegate type. + Callback(async () => + { + await Task.Yield(); + }); +} + +void Callback(Func action) +{ + // out of scope of sample +} +``` + +Refer to [Async/Await - Best Practices in Asynchronous Programming](https://msdn.microsoft.com/en-us/magazine/jj991977.aspx) for more info. diff --git a/docfx/analyzers/VSTHRD102.md b/docfx/analyzers/VSTHRD102.md new file mode 100644 index 000000000..60ba1cb71 --- /dev/null +++ b/docfx/analyzers/VSTHRD102.md @@ -0,0 +1,56 @@ +# VSTHRD102 Implement internal logic asynchronously + +Internal or private methods may be invoked by public methods that are asynchronous. +If the internal method has an opportunity to do work asynchronously, it should do so +in order that async public members can truly be async. + +## Examples of patterns that are flagged by this analyzer + +```csharp +public void PublicMethod() +{ + DoWork(); +} + +public async Task PublicMethodAsync() +{ + DoWork(); + await Task.Yield(); +} + +internal void DoWork() +{ + joinableTaskFactory.Run(async delegate // Analyzer will flag this line + { + await DoSomethingAsync(); + }); +} +``` + +Note how `DoWork()` synchronously blocks for both `PublicMethod()` and `PublicMethodAsync()`. + +## Solution + +Remove the synchronously blocking behavior and make the method async. + +```csharp +public void PublicMethod() +{ + joinableTaskFactory.Run(() => PublicMethodAsync()); +} + +public async Task PublicMethodAsync() +{ + await DoWorkAsync(); + await Task.Yield(); +} + +internal async Task DoWorkAsync() +{ + await DoSomethingAsync(); +} +``` + +Note how `DoWorkAsync()` now allows `PublicMethodAsync()` to do its work asynchronously +while `PublicMethod()` continues to synchronously block, giving your external caller the option +as to whether to do work asynchronously or synchronously. diff --git a/docfx/analyzers/VSTHRD103.md b/docfx/analyzers/VSTHRD103.md new file mode 100644 index 000000000..120310cf3 --- /dev/null +++ b/docfx/analyzers/VSTHRD103.md @@ -0,0 +1,38 @@ +# VSTHRD103 Call async methods when in an async method + +In a method which is already asynchronous, calls to other methods should +be to their async versions, where they exist. + +## Examples of patterns that are flagged by this analyzer + +```csharp +Task DoAsync() +{ + file.Read(buffer, 0, 10); +} +``` + +All methods where an Async-suffixed equivalent exists will produce this warning +when called from a `Task`-returning method. +In addition, calling `Task.Wait()`, `Task.Result` or `Task.GetAwaiter().GetResult()` +will produce this warning. + +## Solution + +Await the async version of the method: + +```csharp +async Task DoAsync() +{ + await file.ReadAsync(buffer, 0, 10); +} +``` + +## Configuration + +This analyzer can be configured to exclude specific APIs from generating diagnostics. +Some APIs may have async versions that are less efficient or inappropriate for certain use cases. + +See our [configuration](configuration.md) topic to learn how to exclude specific methods +using the `vs-threading.SyncMethodsToExcludeFromVSTHRD103.txt` file. +``` diff --git a/docfx/analyzers/VSTHRD104.md b/docfx/analyzers/VSTHRD104.md new file mode 100644 index 000000000..aabbe5722 --- /dev/null +++ b/docfx/analyzers/VSTHRD104.md @@ -0,0 +1,36 @@ +# VSTHRD104 Offer async option + +When a publicly accessible method uses `JoinableTaskFactory.Run`, there should be +another way to access the async behavior without synchronously blocking the thread +so that an async caller can be async throughout. + +This rule encourages this pattern by recognizing when some method *Foo* exists and +calls `JoinableTaskFactory.Run` that there is also a method *FooAsync*. +The recommended pattern then is for *Foo* to call *FooAsync* from the delegate +passed to `JoinableTaskFactory.Run` so that the implementation only need be written once. + +## Examples of patterns that are flagged by this analyzer + +```csharp +public void Foo() { + this.joinableTaskFactory.Run(async delegate { + await Task.Yield(); + }); +} +``` + +## Solution + +Add a FooAsync method, and (optionally) call it from the Foo method: + +```csharp +public void Foo() { + this.joinableTaskFactory.Run(async delegate { + await FooAsync(); + }); +} + +public async Task FooAsync() { + await Task.Yield(); +} +``` diff --git a/docfx/analyzers/VSTHRD105.md b/docfx/analyzers/VSTHRD105.md new file mode 100644 index 000000000..06c3e34a4 --- /dev/null +++ b/docfx/analyzers/VSTHRD105.md @@ -0,0 +1,83 @@ +# VSTHRD105 Avoid method overloads that assume `TaskScheduler.Current` + +Certain methods in the .NET Framework have overloads that allow specifying or omitting +a `TaskScheduler` instance. Always specify one explicitly to avoid the assumed `TaskScheduler.Current` +value, whose behavior is defined by your caller and may vary at runtime. + +The "current" `TaskScheduler` is defined by the one that is executing the currently running code. +But when your code is executing without having been scheduled by a `TaskScheduler` (as is the case with most code), +then the `TaskScheduler.Current` property returns `TaskScheduler.Default` which schedules tasks on the thread pool. +This leads many to incorrectly assume that task scheduling methods such as `StartNew` and `ContinueWith` default +to using the thread pool when in fact their default behavior varies by your caller. + +This variability in behavior leads to bugs when, for example, `TaskScheduler.Current` returns a `TaskScheduler` +that executes tasks on the application's main thread and/or only executes one task at once, such as one obtained +from the `TaskScheduler.FromCurrentSynchronizationContext()` method. +Such a circumstance often leads to deadlocks or responsiveness issues in the application. + +Always explicitly specifying `TaskScheduler.Default` (or other if appropriate) ensures your code will schedule +tasks in a predictable, consistent way. + +No diagnostic is produced by this analyzer when `TaskFactory.StartNew` is invoked on a private instance +of `TaskFactory`, since it may in fact have a safe default for `TaskScheduler`. + +Similar rules: [CA2008 (DoNotCreateTasksWithoutPassingATaskSchedulerAnalyzer)](https://github.com/dotnet/roslyn-analyzers/blob/32d8f1e397439035f0ecb5f61a9e672225f0ecdb/src/Microsoft.NetCore.Analyzers/Core/Tasks/DoNotCreateTasksWithoutPassingATaskScheduler.cs) + +## Examples of patterns that are flagged by this analyzer + +```csharp +private void FirstMethod() +{ + TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(); + Task.Factory.StartNew( + () => + { + this.AnotherMethod(); + }, + System.Threading.CancellationToken.None, + TaskCreationOptions.None, + uiScheduler); +} + +private void AnotherMethod() +{ + // TaskScheduler.Current is assumed here, which is determined by our caller. + var nestedTask = Task.Factory.StartNew( // analyzer flags this line + () => + { + // Ooops, we're still on the UI thread when called by FirstMethod. + // But we might be on the thread pool if someone else called us. + }); +} +``` + +## Solution + +Specify a `TaskScheduler` explicitly to suppress the warning: + +```csharp +private void FirstMethod() +{ + TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(); + Task.Factory.StartNew( + () => + { + this.AnotherMethod(); + }, + CancellationToken.None, + TaskCreationOptions.None, + uiScheduler); +} + +private void AnotherMethod() +{ + var nestedTask = Task.Factory.StartNew( + () => + { + // Ah, now we're reliably running on the thread pool. :) + }, + CancellationToken.None, + TaskCreationOptions.None, + TaskScheduler.Default); // Specify TaskScheduler explicitly here. +} +``` diff --git a/docfx/analyzers/VSTHRD106.md b/docfx/analyzers/VSTHRD106.md new file mode 100644 index 000000000..f47fc46ce --- /dev/null +++ b/docfx/analyzers/VSTHRD106.md @@ -0,0 +1,31 @@ +# VSTHRD106 Use `InvokeAsync` to raise async events + +Asynchronous events (those typed as `AsyncEventHandler`) must be raised carefully to ensure +all event handlers are invoked and awaited on. + +Although C# lets you invoke event handlers naturally, it has no awareness of async event handlers +and thus will not let you correctly await on their invocation nor invoke them sequentially. + +## Examples of patterns that are flagged by this analyzer + +```csharp +public AsyncEventHandler Clicked; + +async Task OnClicked() { + await Clicked(this, EventArgs.Empty); // only awaits the first event handler. +} +``` + +## Solution + +Use the `InvokeAsync` extension method defined in the `TplExtensions` class and await its result. +This will ensure each event handler completes before invoking the next event handler in the list, +similar to the default behavior for raising synchronous events. + +```csharp +public AsyncEventHandler Clicked; + +async Task OnClicked() { + await Clicked.InvokeAsync(this, EventArgs.Empty); // await for the completion of all handlers. +} +``` diff --git a/docfx/analyzers/VSTHRD107.md b/docfx/analyzers/VSTHRD107.md new file mode 100644 index 000000000..8694cf2ea --- /dev/null +++ b/docfx/analyzers/VSTHRD107.md @@ -0,0 +1,27 @@ +# VSTHRD107 Await Task within using expression + +The C# `using` statement requires that the used expression implement `IDisposable`. +Because `Task` implements `IDisposable`, one may accidentally omit an `await` operator +and `Dispose` of the `Task` instead of the `T` result itself when `T` derives from `IDisposable`. + +## Examples of patterns that are flagged by this analyzer + +```csharp +AsyncSemaphore lck; +using (lck.EnterAsync()) +{ + // ... +} +``` + +## Solution + +Add the `await` operator within the `using` expression. + +```csharp +AsyncSemaphore lck; +using (await lck.EnterAsync()) +{ + // ... +} +``` diff --git a/docfx/analyzers/VSTHRD108.md b/docfx/analyzers/VSTHRD108.md new file mode 100644 index 000000000..c2b986e2a --- /dev/null +++ b/docfx/analyzers/VSTHRD108.md @@ -0,0 +1,46 @@ +# VSTHRD108 Assert thread affinity unconditionally + +When a method has thread affinity and throws if called from the wrong thread, it should do so without regard to any other condition. This helps ensure the caller will notice early during development that they are calling from the wrong thread. Extra conditions can hide the problem till end users discover an application failure. + +## Examples of patterns that are flagged by this analyzer + +```csharp +private int? age; + +public int GetAge() +{ + if (!this.age.HasValue) + { + ThreadHelper.ThrowIfNotOnUIThread(); + this.age = DoExpensiveUIThreadWork(); + } + + return this.age.Value; +} +``` + +The problem here is that although the UI thread is only strictly required when the field is actually initialized, callers generally cannot predict whether they will be the first or a subsequent caller. If they call from a background thread and tend to be a subsequent caller, no exception will be thrown. But under some conditions in the app when they happen to be the first caller, they'll fail at runtime because they're calling from the background thread. + +## Solution + +Move the code that throws when not on the UI thread outside the conditional block. + +```csharp +private int? age; + +public int GetAge() +{ + ThreadHelper.ThrowIfNotOnUIThread(); + if (!this.age.HasValue) + { + this.age = DoExpensiveUIThreadWork(); + } + + return this.age.Value; +} +``` + +## Configuration + +This analyzer is configurable via the `vs-threading.MainThreadAssertingMethods.txt` file. +See our [configuration](configuration.md) topic for more information. diff --git a/docfx/analyzers/VSTHRD109.md b/docfx/analyzers/VSTHRD109.md new file mode 100644 index 000000000..7e28198b7 --- /dev/null +++ b/docfx/analyzers/VSTHRD109.md @@ -0,0 +1,29 @@ +# VSTHRD109 Switch instead of assert in async methods + +Methods that are or can be async should switch to the main thread when necessary +rather than throw an exception if invoked from a different thread. +This allows callers to invoke any async method from any thread +without having to concern themselves with the threading requirements of a method that +can support its own threading requirements by switching. + +## Examples of patterns that are flagged by this analyzer + +```csharp +async Task FooAsync() { + ThreadHelper.ThrowIfNotOnUIThread(); + DoStuff(); + await DoMoreStuff(); +} +``` + +## Solution + +Use `await SwitchToMainThreadAsync()` instead: + +```csharp +async Task FooAsync() { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + DoStuff(); + await DoMoreStuff(); +} +``` diff --git a/docfx/analyzers/VSTHRD110.md b/docfx/analyzers/VSTHRD110.md new file mode 100644 index 000000000..6e7bc44af --- /dev/null +++ b/docfx/analyzers/VSTHRD110.md @@ -0,0 +1,75 @@ +# VSTHRD110 Observe result of async calls + +Tasks returned from async methods should be awaited, or assigned to a variable for observation later. +Methods that return `Task`s often complete and report their work via the `Task` they return, and simply +invoking the method does not guarantee that its work is complete nor successful. Using the `await` keyword +just before the method call causes execution of the calling method to effectively suspend until the called +method has completed and rethrows any exception thrown by the method. + +When a `Task` or `Task` is returned and is not awaited or redirected in some other way, +within the context of a synchronous method, a warning is reported. + +This rule does *not* apply to calls made within async methods, since [CS4014][CS4014] already reports these. + +## Examples of patterns that are flagged by this analyzer + +```csharp +void Foo() { + DoStuffAsync(); +} + +async Task DoStuffAsync() { /* ... */ } +``` + +## Solution + +Convert the method to be async and await the expression: + +```csharp +async Task FooAsync() { + await DoStuffAsync(); +} + +async Task DoStuffAsync() { /* ... */ } +``` + +When the calling method's signature cannot be changed, wrap the method body in a `JoinableTaskFactory.Run` delegate instead: + +```csharp +void Foo() { + jtf.Run(async delegate { + await DoStuffAsync(); + }); +} + +async Task DoStuffAsync() { /* ... */ } +``` + +One other option is to assign the result of the method call to a field or local variable, presumably to track it later: + +```csharp +void Foo() { + Task watchThis = DoStuffAsync(); +} + +async Task DoStuffAsync() { /* ... */ } +``` + +When tracking the `Task` with a field, remember that to await it later without risk of deadlocking, +wrap it in a `JoinableTask` using `JoinableTaskFactory.RunAsync`, per [the 3rd rule](../docs/threading_rules.md#Rule3). + +```csharp +JoinableTask watchThis; + +void Foo() { + this.watchThis = jtf.RunAsync(() => DoStuffAsync()); +} + +async Task WaitForFooToFinishAsync() { + await this.watchThis; +} + +async Task DoStuffAsync() { /* ... */ } +``` + +[CS4014]: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/cs4014 diff --git a/docfx/analyzers/VSTHRD111.md b/docfx/analyzers/VSTHRD111.md new file mode 100644 index 000000000..aaa46ced5 --- /dev/null +++ b/docfx/analyzers/VSTHRD111.md @@ -0,0 +1,38 @@ +# VSTHRD111 Use `.ConfigureAwait(bool)` + +Some code bases, particularly libraries with no affinity to an app's UI thread, are advised to use `.ConfigureAwait(false)` for each and every _await_ because it can avoid deadlocks after those calls start on an application's UI thread and the app later decides to synchronously block the UI thread waiting for those tasks to finish. Using `.ConfigureAwait(false)` also allows continuations to switch to a background thread even when no synchronous blocking would cause a deadlock, which makes for a more responsive application and possibly higher throughput of async operations. + +Note that this scenario can also be solved using the `JoinableTaskFactory`, but many class libraries may not wish to depend on the application proffers an instance of that type to the library. Where JoinableTaskFactory _does_ apply, use of `.ConfigureAwait(false)` is _not_ recommended. See [this topic](../docs/cookbook_vs.md#should-i-await-a-task-with-configureawaitfalse) for more on when `.ConfigureAwait(false)` and `.ConfigureAwait(true)` are appropriate. + +**This analyzer's diagnostics are *hidden* by default**. You should enable the rule for libraries that use to require this await suffix. + +## Examples of patterns that are flagged by this analyzer + +Any await on `Task` or `ValueTask` without the `.ConfigureAwait(bool)` method called on it will be flagged. + +```csharp +async Task FooAsync() { + await DoStuffAsync(); // This line is flagged + await DoMoreStuffAsync(); // This line is flagged +} + +async Task DoStuffAsync() { /* ... */ } +async ValueTask DoMoreStuffAsync() { /* ... */ } +``` + +## Solution + +Add `.ConfigureAwait(false)` or `.ConfigureAwait(true)` to the awaited `Task` or `ValueTask`. + +```csharp +async Task FooAsync() { + await DoStuffAsync().ConfigureAwait(true); + await DoMoreStuffAsync().ConfigureAwait(false); +} + +async Task DoStuffAsync() { /* ... */ } +async ValueTask DoMoreStuffAsync() { /* ... */ } +``` + +Code fixes are offered for for this diagnostic to add either `.ConfigureAwait(false)` or `.ConfigureAwait(true)` +to an awaited expression. diff --git a/docfx/analyzers/VSTHRD112.md b/docfx/analyzers/VSTHRD112.md new file mode 100644 index 000000000..b9e3ae45c --- /dev/null +++ b/docfx/analyzers/VSTHRD112.md @@ -0,0 +1,66 @@ +# VSTHRD112 Implement `System.IAsyncDisposable` + +The `Microsoft.VisualStudio.Threading.IAsyncDisposable` interface is obsolete now that the +`System.IAsyncDisposable` interface has been defined for .NET Standard 2.0 and .NET Framework 4.6.1 +by the [`Microsoft.Bcl.AsyncInterfaces` NuGet package](https://www.nuget.org/packages/Microsoft.Bcl.AsyncInterfaces). + +New classes looking to support async disposable should use `System.IAsyncDisposable` instead of `Microsoft.VisualStudio.Threading.IAsyncDisposable`. +Existing classes that already implement `Microsoft.VisualStudio.Threading.IAsyncDisposable` should *also* implement `System.IAsyncDisposable` so the async disposal option will be recognized by code that only checks for presence of the new interface. + +## Examples of patterns that are flagged by this analyzer + +This class only implements `Microsoft.VisualStudio.Threading.IAsyncDisposable` and will produce the VSTHRD112 diagnostic: + +```cs +using Microsoft.VisualStudio.Threading; + +class SomeClass : IAsyncDisposable +{ + public Task DisposeAsync() + { + } +} +``` + +## Solution + +Implement `System.IAsyncDisposable` in addition to (or instead of) `Microsoft.VisualStudio.Threading.IAsyncDisposable`. +Add a package reference to `Microsoft.Bcl.AsyncInterfaces` if the compiler cannot find `System.IAsyncDisposable`. + +In this example, only `System.IAsyncDisposable` is supported, which is acceptable: + +```cs +using System; + +class SomeClass : IAsyncDisposable +{ + public ValueTask DisposeAsync() + { + } +} +``` + +In this next example, both interfaces are supported: + +```cs +class SomeClass : System.IAsyncDisposable, Microsoft.VisualStudio.Threading.IAsyncDisposable +{ + Task Microsoft.VisualStudio.Threading.IAsyncDisposable.DisposeAsync() + { + // Simply forward the call to the other DisposeAsync overload. + System.IAsyncDisposable self = this; + return self.DisposeAsync().AsTask(); + } + + ValueTask System.IAsyncDisposable.DisposeAsync() + { + // Interesting dispose logic here. + } +} +``` + +In the above example both `DisposeAsync` methods are explicit interface implementations. +Promoting one of the methods to be `public` is typically advised. +If one of these methods was already public and the class itself is public or protected, keep the same method public to avoid an API binary breaking change. + +An automated code fix may be offered for VSTHRD112 diagnostics. diff --git a/docfx/analyzers/VSTHRD113.md b/docfx/analyzers/VSTHRD113.md new file mode 100644 index 000000000..8d4dee677 --- /dev/null +++ b/docfx/analyzers/VSTHRD113.md @@ -0,0 +1,37 @@ +# VSTHRD113 Check for `System.IAsyncDisposable` + +The `Microsoft.VisualStudio.Threading.IAsyncDisposable` interface is obsolete now that the +`System.IAsyncDisposable` interface has been defined for .NET Standard 2.0 and .NET Framework 4.6.1 +by the [`Microsoft.Bcl.AsyncInterfaces` NuGet package](https://www.nuget.org/packages/Microsoft.Bcl.AsyncInterfaces). + +Existing code that tests for the `Microsoft.VisualStudio.Threading.IAsyncDisposable` interface on some object should also check for `System.IAsyncDisposable` and behave similarly in either case. +New code should consider only supporting the new `System.IAsyncDisposable` interface. + +## Examples of patterns that are flagged by this analyzer + +The following code only checks for the obsolete interface and is flagged by this diagnostic: + +```cs +using Microsoft.VisualStudio.Threading; + +if (obj is IAsyncDisposable asyncDisposable) +{ + await asyncDisposable.DisposeAsync(); +} +``` + +## Solution + +Fix this by adding a code branch for the new interface that behaves similarly +within the same containing code block: + +```cs +if (obj is Microsoft.VisualStudio.Threading.IAsyncDisposable vsThreadingAsyncDisposable) +{ + await vsThreadingAsyncDisposable.DisposeAsync(); +} +else if (obj is System.IAsyncDisposable bclAsyncDisposable) +{ + await bclAsyncDisposable.DisposeAsync(); +} +``` diff --git a/docfx/analyzers/VSTHRD114.md b/docfx/analyzers/VSTHRD114.md new file mode 100644 index 000000000..c20c3fb53 --- /dev/null +++ b/docfx/analyzers/VSTHRD114.md @@ -0,0 +1,31 @@ +# VSTHRD114 Avoid returning a null Task + +Returning `null` from a non-async `Task`/`Task` method will cause a `NullReferenceException` at runtime. This problem can be avoided by returning `Task.CompletedTask`, `Task.FromResult(null)` or `Task.FromResult(default(T))` instead. + +## Examples of patterns that are flagged by this analyzer + +Any non-async `Task` returning method with an explicit `return null;` will be flagged. + +```csharp +Task DoAsync() { + return null; +} + +Task GetSomethingAsync() { + return null; +} +``` + +## Solution + +Return a task like `Task.CompletedTask` or `Task.FromResult`. + +```csharp +Task DoAsync() { + return Task.CompletedTask; +} + +Task GetSomethingAsync() { + return Task.FromResult(null); +} +``` diff --git a/docfx/analyzers/VSTHRD115.md b/docfx/analyzers/VSTHRD115.md new file mode 100644 index 000000000..5e1560911 --- /dev/null +++ b/docfx/analyzers/VSTHRD115.md @@ -0,0 +1,38 @@ +# VSTHRD115 Avoid creating a JoinableTaskContext with an explicit `null` `SynchronizationContext` + +Constructing a `JoinableTaskContext` with an explicit `null` `SynchronizationContext` is not recommended as a means to construct an instance for use in unit tests or processes without a main thread. +This is because the constructor will automatically use `SynchronizationContext.Current` in lieu of a non-`null` argument. +If `SynchronizationContext.Current` happens to be non-`null`, the constructor may unexpectedly configure the new instance as if a main thread were present. + +## Examples of patterns that are flagged by this analyzer + +```csharp +void SetupJTC() { + this.jtc = new JoinableTaskContext(null, null); +} +``` + +This code *appears* to configure the `JoinableTaskContext` to not be associated with any `SynchronizationContext`. +But in fact it will be associated with the current `SynchronizationContext` if one is present. + +## Solution + +If you intended to inherit `SynchronizationContext.Current` to initialize with a main thread, +provide that value explicitly as the second argument to suppress the warning: + +```cs +void SetupJTC() { + this.jtc = new JoinableTaskContext(null, SynchronizationContext.Current); +} +``` + +If you intended to create a `JoinableTaskContext` for use in a unit test or in a process without a main thread, +call `JoinableTaskContext.CreateNoOpContext()` instead: + +```cs +void SetupJTC() { + this.jtc = JoinableTaskContext.CreateNoOpContext(); +} +``` + +Code fixes are offered to update code to either of the above patterns. diff --git a/docfx/analyzers/VSTHRD200.md b/docfx/analyzers/VSTHRD200.md new file mode 100644 index 000000000..3f87cf273 --- /dev/null +++ b/docfx/analyzers/VSTHRD200.md @@ -0,0 +1,47 @@ +# VSTHRD200 Use `Async` suffix for async methods + +The .NET Guidelines for async methods includes that such methods +should have names that include an "Async" suffix. + +Methods that return awaitable types such as `Task` or `ValueTask` +should have an Async suffix. +Methods that do not return awaitable types should not use the Async suffix. + +## Examples of patterns that are flagged by this analyzer + +This `Task`-returning method should have a name that ends with Async: + +```csharp +async Task DoSomething() // analyzer flags this line +{ + await Task.Yield(); +} +``` + +This method should not have a name that ends with Async, since it does not return an awaitable type: + +```csharp +bool DoSomethingElseAsync() // analyzer flags this line +{ + return false; +} +``` + +## Solution + +Simply rename the method to end in "Async" (or remove the suffix, as appropriate): + +```csharp +async Task DoSomethingAsync() +{ + await Task.Yield(); +} + +bool DoSomethingElse() +{ + return false; +} +``` + + +A code fix exists to automatically rename such methods. diff --git a/doc/analyzers/configuration.md b/docfx/analyzers/configuration.md similarity index 86% rename from doc/analyzers/configuration.md rename to docfx/analyzers/configuration.md index 75d63c693..77acf4d82 100644 --- a/doc/analyzers/configuration.md +++ b/docfx/analyzers/configuration.md @@ -79,3 +79,16 @@ thread. **Line format:** `[Namespace.TypeName]::MethodName` **Sample:** `[System.Windows.Threading.Dispatcher]::Invoke` + +## Methods to exclude from VSTHRD103 checks + +The VSTHRD103 analyzer flags calls to synchronous methods where asynchronous equivalents exist, +when in an async context. Sometimes certain APIs have async versions but those async versions +are significantly slower, less efficient, or simply not preferred. These methods can be +excluded from VSTHRD103 analysis by specifying them in a configuration file. + +**Filename:** `vs-threading.SyncMethodsToExcludeFromVSTHRD103.txt` + +**Line format:** `[Namespace.TypeName]::MethodName` + +**Sample:** `[System.Data.SqlClient.SqlDataReader]::Read` diff --git a/doc/analyzers/fsa.md b/docfx/analyzers/fsa.md similarity index 91% rename from doc/analyzers/fsa.md rename to docfx/analyzers/fsa.md index f1b49668d..48afeb68d 100644 --- a/doc/analyzers/fsa.md +++ b/docfx/analyzers/fsa.md @@ -6,4 +6,4 @@ Diagnostics reported by such an analyzer will appear in a full build log. They m To ensure the diagnostics are always visible, even when documents are open, select the "Enable full Solution Analysis" option, as shown below: -![Visual Studio Options -> Text Editor -> C# -> Advanced -> Enable full solution analysis option](images/fsa.png) \ No newline at end of file +![Visual Studio Options -> Text Editor -> C# -> Advanced -> Enable full solution analysis option](../images/fsa.png) diff --git a/doc/analyzers/index.md b/docfx/analyzers/index.md similarity index 73% rename from doc/analyzers/index.md rename to docfx/analyzers/index.md index 038fdd038..4d2077c25 100644 --- a/doc/analyzers/index.md +++ b/docfx/analyzers/index.md @@ -2,31 +2,33 @@ The following are the diagnostic analyzers installed with the [Microsoft.VisualStudio.Threading.Analyzers][1] NuGet package. +Learn more about [how to install and configure these analyzers](installation.md). ID | Title | Severity | Supports | Default diagnostic severity ---- | --- | --- | --- | --- | -[VSTHRD001](VSTHRD001.md) | Avoid legacy thread switching methods | Critical | [1st rule](../threading_rules.md#Rule1) | 🔡 Warning -[VSTHRD002](VSTHRD002.md) | Avoid problematic synchronous waits | Critical | [2nd rule](../threading_rules.md#Rule2) | Warning -[VSTHRD003](VSTHRD003.md) | Avoid awaiting foreign Tasks | Critical | [3rd rule](../threading_rules.md#Rule3) | Warning -[VSTHRD004](VSTHRD004.md) | Await SwitchToMainThreadAsync | Critical | [1st rule](../threading_rules.md#Rule1) | Error -[VSTHRD010](VSTHRD010.md) | Invoke single-threaded types on Main thread | Critical | [1st rule](../threading_rules.md#Rule1) | Warning -[VSTHRD011](VSTHRD011.md) | Use `AsyncLazy` | Critical | [3rd rule](../threading_rules.md#Rule3) | Error -[VSTHRD012](VSTHRD012.md) | Provide JoinableTaskFactory where allowed | Critical | [All rules](../threading_rules.md) | Warning +[VSTHRD001](VSTHRD001.md) | Avoid legacy thread switching methods | Critical | [1st rule](../docs/threading_rules.md#Rule1) | 🔡 Warning +[VSTHRD002](VSTHRD002.md) | Avoid problematic synchronous waits | Critical | [2nd rule](../docs/threading_rules.md#Rule2) | Warning +[VSTHRD003](VSTHRD003.md) | Avoid awaiting foreign Tasks | Critical | [3rd rule](../docs/threading_rules.md#Rule3) | Warning +[VSTHRD004](VSTHRD004.md) | Await SwitchToMainThreadAsync | Critical | [1st rule](../docs/threading_rules.md#Rule1) | Error +[VSTHRD010](VSTHRD010.md) | Invoke single-threaded types on Main thread | Critical | [1st rule](../docs/threading_rules.md#Rule1) | Warning +[VSTHRD011](VSTHRD011.md) | Use `AsyncLazy` | Critical | [3rd rule](../docs/threading_rules.md#Rule3) | Error +[VSTHRD012](VSTHRD012.md) | Provide JoinableTaskFactory where allowed | Critical | [All rules](../docs/threading_rules.md) | Warning [VSTHRD100](VSTHRD100.md) | Avoid `async void` methods | Advisory | | Warning [VSTHRD101](VSTHRD101.md) | Avoid unsupported async delegates | Advisory | [VSTHRD100](VSTHRD100.md) | Warning -[VSTHRD102](VSTHRD102.md) | Implement internal logic asynchronously | Advisory | [2nd rule](../threading_rules.md#Rule2) | Info +[VSTHRD102](VSTHRD102.md) | Implement internal logic asynchronously | Advisory | [2nd rule](../docs/threading_rules.md#Rule2) | Info [VSTHRD103](VSTHRD103.md) | Call async methods when in an async method | Advisory | | Warning [VSTHRD104](VSTHRD104.md) | Offer async option | Advisory | | Info [VSTHRD105](VSTHRD105.md) | Avoid method overloads that assume `TaskScheduler.Current` | Advisory | | Warning [VSTHRD106](VSTHRD106.md) | Use `InvokeAsync` to raise async events | Advisory | | Warning [VSTHRD107](VSTHRD107.md) | Await Task within using expression | Advisory | | Error -[VSTHRD108](VSTHRD108.md) | Assert thread affinity unconditionally | Advisory | [1st rule](../threading_rules.md#Rule1), [VSTHRD010](VSTHRD010.md) | Warning -[VSTHRD109](VSTHRD109.md) | Switch instead of assert in async methods | Advisory | [1st rule](../threading_rules.md#Rule1) | Error +[VSTHRD108](VSTHRD108.md) | Assert thread affinity unconditionally | Advisory | [1st rule](../docs/threading_rules.md#Rule1), [VSTHRD010](VSTHRD010.md) | Warning +[VSTHRD109](VSTHRD109.md) | Switch instead of assert in async methods | Advisory | [1st rule](../docs/threading_rules.md#Rule1) | Error [VSTHRD110](VSTHRD110.md) | Observe result of async calls | Advisory | | Warning [VSTHRD111](VSTHRD111.md) | Use `.ConfigureAwait(bool)` | Advisory | | Hidden [VSTHRD112](VSTHRD112.md) | Implement `System.IAsyncDisposable` | Advisory | | Info [VSTHRD113](VSTHRD113.md) | Check for `System.IAsyncDisposable` | Advisory | | Info [VSTHRD114](VSTHRD114.md) | Avoid returning null from a `Task`-returning method. | Advisory | | Warning +[VSTHRD115](VSTHRD115.md) | Avoid creating a JoinableTaskContext with an explicit `null` `SynchronizationContext` | Advisory | | Warning [VSTHRD200](VSTHRD200.md) | Use `Async` naming convention | Guideline | [VSTHRD103](VSTHRD103.md) | Warning ## Severity descriptions @@ -47,4 +49,4 @@ Guideline | 200-299 | Code that deviates from best practices and may limit the b Some analyzers' behavior can be configured. See our [configuration](configuration.md) topic for more information. -[1]: https://nuget.org/packages/microsoft.visualstudio.threading.analyzers +[1]: https://www.nuget.org/packages/microsoft.visualstudio.threading.analyzers diff --git a/docfx/analyzers/installation.md b/docfx/analyzers/installation.md new file mode 100644 index 000000000..9724a95b8 --- /dev/null +++ b/docfx/analyzers/installation.md @@ -0,0 +1,68 @@ +# Installing the threading analyzers + +The threading analyzers built from this repo and consumable via [the Microsoft.VisualStudio.Threading.Analyzers nuget package][NuGet] are useful for virtually every app library and most libraries. +In particular, projects do not need to be related to Visual Studio in order for these analyzers to apply and improve the quality of your code. + +This document outlines how to install the analyzers and offer guidance on how to optimally configure them. + +## How to install the threading analyzers + +If you are using the `Microsoft.VisualStudio.Threading` NuGet package, you should already have the analyzers installed because they are brought in as a dependency of this package. + +Some projects may not want a runtime dependency on `Microsoft.VisualStudio.Threading`, but the analyzers may still apply. +Install the analyzers using any of the methods described on [the package landing page on nuget.org][NuGet]. +For example, you might add this tag to your project file: + +```xml + +``` + +Or even better, add this to some broad `Directory.Build.targets` file so it can apply to all of your projects. + +Remember to periodically update the version of the analyzer package you reference. +You should generally use the latest version available, without regard to the version of the application or threading library in use, so you get the best diagnostics. + +## Configuring the analyzers + +There are [many rules](index.md) in the analyzer package. +The default severity levels for the various rules are not appropriate for every type of project. +To get the best default severity levels for your project type, please review [these editorconfig recommendations](https://github.com/microsoft/vs-threading/blob/main/doc/editorconfigs/README.md) and apply them to your project. + +Some analyzers allow for [specialized configuration](configuration.md) that allows you to tailor them to your specific application or library to provide even more value to your team. + +## Dealing with suppressions + +In some projects we find that some of the threading analyzers have been disabled because they were producing warnings that the project owner did not want to fix at the time. +We generally discourage disabling rules that apply to a project because of the deadlocks that may already exist or that can creep into a codebase over time. +If installing analyzers produces blocking errors or warnings and you cannot fix them all at once, suppress the warnings and schedule time to go back to review them soon. +The recommended way to suppress your "baseline" of warnings is with in-situ `#pragma` suppressions such as: + +```css +#pragma warning disable VSTHRD010 // Suppress warning in baseline when installing analyzers -- should review soon +bad.Code(); +#pragma warning restore VSTHRD010 +``` + +Suppressing specific occurrences in this way can be done in bulk using an automated C# code fix within Visual Studio. +Doing it for each individual occurrence is better than suppressing the entire rule ID because the analyzers will be allowed to flag newly introduced code while you have not yet reviewed your old code and re-enabled the rule. + +If you suspect your project may have suppressed an analyzer rule project-wide, please look in the following common places for broad suppressions and remove them so that your project gets the analyzers run on them: + +1. MSBuild project file `` properties that contain `VSTHRD*` warning IDs. +1. MSBuild project files with `` to the analyzer package but set `ExcludeAssets="analyzers"`, `ExcludeAssets="all"` or `IncludeAssets="none"` to explicitly turn them off. +1. .ruleset files that decrease severity or turn off `VSTHRD*` rule IDs. +1. .editorconfig files that decrease severity or turn off `VSTHRD*` rule IDs. + +## Visual Studio specific analyzers + +When a project targets Visual Studio specifically, it should also reference the [Microsoft.VisualStudio.Sdk.Analyzers][SdkAnalyzers] NuGet package. +This package delivers additional analyzers and configures _these_ threading analyzers to be more aware of VS-specific APIs so that better diagnostics can be produced. + +```xml + +``` + +Note that although the SDK analyzers automatically brings in the threading analyzers due to a package dependency, you should reference the threading analyzers directly as well to bring in the latest version, since the SDK analyzers ships infrequently and thus brings in older versions of the threading analyzers by default. + +[NuGet]: https://www.nuget.org/packages/microsoft.visualstudio.threading.analyzers +[SdkAnalyzers]: https://www.nuget.org/packages/microsoft.visualstudio.sdk.analyzers diff --git a/docfx/analyzers/toc.yml b/docfx/analyzers/toc.yml new file mode 100644 index 000000000..cc7bee4ff --- /dev/null +++ b/docfx/analyzers/toc.yml @@ -0,0 +1,29 @@ +items: +- href: index.md +- href: configuration.md +- href: fsa.md +- href: installation.md +- href: VSTHRD001.md +- href: VSTHRD002.md +- href: VSTHRD003.md +- href: VSTHRD004.md +- href: VSTHRD010.md +- href: VSTHRD011.md +- href: VSTHRD012.md +- href: VSTHRD100.md +- href: VSTHRD101.md +- href: VSTHRD102.md +- href: VSTHRD103.md +- href: VSTHRD104.md +- href: VSTHRD105.md +- href: VSTHRD106.md +- href: VSTHRD107.md +- href: VSTHRD108.md +- href: VSTHRD109.md +- href: VSTHRD110.md +- href: VSTHRD111.md +- href: VSTHRD112.md +- href: VSTHRD113.md +- href: VSTHRD114.md +- href: VSTHRD115.md +- href: VSTHRD200.md diff --git a/docfx/docfx.json b/docfx/docfx.json new file mode 100644 index 000000000..e7568b08e --- /dev/null +++ b/docfx/docfx.json @@ -0,0 +1,48 @@ +{ + "metadata": [ + { + "src": [ + { + "src": "../src/Microsoft.VisualStudio.Threading", + "files": [ + "**/*.csproj" + ] + } + ], + "dest": "api" + } + ], + "build": { + "content": [ + { + "files": [ + "**/*.{md,yml}" + ], + "exclude": [ + "_site/**" + ] + } + ], + "resource": [ + { + "files": [ + "images/**" + ] + } + ], + "xref": [ + "https://learn.microsoft.com/en-us/dotnet/.xrefmap.json" + ], + "output": "_site", + "template": [ + "default", + "modern" + ], + "globalMetadata": { + "_appName": "Microsoft.VisualStudio.Threading", + "_appTitle": "Microsoft.VisualStudio.Threading", + "_enableSearch": true, + "pdf": false + } + } +} diff --git a/doc/async_hang.md b/docfx/docs/async_hang.md similarity index 100% rename from doc/async_hang.md rename to docfx/docs/async_hang.md diff --git a/doc/cookbook_vs.md b/docfx/docs/cookbook_vs.md similarity index 99% rename from doc/cookbook_vs.md rename to docfx/docs/cookbook_vs.md index 778c795bb..c81183d50 100644 --- a/doc/cookbook_vs.md +++ b/docfx/docs/cookbook_vs.md @@ -1,11 +1,6 @@ Cookbook for Visual Studio ========================== -Important for CPS extension authors and clients: please -replace all references to `ThreadHelper.JoinableTaskFactory` with -`this.ThreadHandling.AsyncPump`, where `this.ThreadHandling` is an `[Import] -IThreadHandling`. - ## Initial setup - Add a reference to [Microsoft.VisualStudio.Threading][NuPkg]. diff --git a/doc/dumpasync.md b/docfx/docs/dumpasync.md similarity index 83% rename from doc/dumpasync.md rename to docfx/docs/dumpasync.md index efbf4c920..c7693131f 100644 --- a/doc/dumpasync.md +++ b/docfx/docs/dumpasync.md @@ -8,7 +8,8 @@ for an app hang that is waiting for async methods to complete. Using this tool requires that you download and install [WinDbg][WinDbg], which can attach to a running process or DMP file. The `!dumpasync` extension is only available for WinDbg. -The `!dumpasync` extension itself is exported from the `SosThreadingTools.dll` library, which is included in a zip that you can acquire from [our releases page](https://github.com/Microsoft/vs-threading/releases). +The `!dumpasync` extension itself is exported from the `SosThreadingTools.dll` library, which is included in a `SosThreadingTools.*.nupkg` archive that you can acquire from [our releases page](https://github.com/Microsoft/vs-threading/releases). +It will appear under the `tools/win-x64` or `tools/win-x86` folder within that archive. ## Usage @@ -68,4 +69,26 @@ In the first stack above, `SendRequestAsync` method is on top, so it is the meth | -1 | The async method is currently executing (you should find it on a real thread's callstack somewhere). | >= 0 | The 0-index into which "await" has most recently yielded. The list of awaits for the method are in strict syntax appearance order. That is, regardless of code execution, if branching, etc., it's the index into which await in code syntax order has yielded. For example, if you position the caret at the top of the method definition and search for "await ", and count how many times you hit a match, starting from 0, when you arrive at the number that you found in the state field, you've found the await that has most recently yielded. Note that when the code being debugged is compiled with certain Dev14 prerelease versions of the Roslyn compiler, this index is 1-based instead of 0-based. +In some cases, the top most frame may point to an `await` on an async method without other frames present at the top such as: + +``` +0730c750 <0> MyClass+d__23 +.0730c17c <0> MyClass+d__2 +``` + +where `ExecuteAsync` may look like: + +```csharp +async Task ExecuteAsync(...) +{ + await ExecuteInternalAsync(..); +} +``` + +This could mean either: + +- `ExecuteInternalAsync` method is executing code before its first `await`, in which case it should be visible on thread callstacks. +- `ExecuteInternalAsync` is completed and continuation is scheduled for execution but is not started yet. In Visual Studio case this could be because continuation is scheduled +for main thread but main thread is busy and/or not allowing the task to be executed yet. + [WinDbg]: https://aka.ms/windbg-direct-download diff --git a/docfx/docs/features.md b/docfx/docs/features.md new file mode 100644 index 000000000..0e517bcfb --- /dev/null +++ b/docfx/docs/features.md @@ -0,0 +1,26 @@ +# Features + +Async synchronization primitives, async collections, TPL and dataflow extensions. The JoinableTaskFactory allows synchronously blocking the UI thread for async work. This package is applicable to any .NET application (not just Visual Studio). + +* Async versions of many threading synchronization primitives + * `AsyncAutoResetEvent` + * `AsyncBarrier` + * `AsyncCountdownEvent` + * `AsyncManualResetEvent` + * `AsyncReaderWriterLock` + * `AsyncSemaphore` + * `ReentrantSemaphore` +* Async versions of very common types + * `AsyncEventHandler` + * `AsyncLazy` + * `AsyncLazyInitializer` + * `AsyncLocal` + * `AsyncQueue` +* Await extension methods + * Await on a `TaskScheduler` to switch to it. + Switch to a background thread with `await TaskScheduler.Default;` + * Await on a `Task` with a timeout + * Await on a `Task` with cancellation +* `JoinableTaskFactory` that allows you to schedule asynchronous or synchronous work + that does not deadlock with the UI thread even when the UI thread needs to + synchronously block on the result. diff --git a/docfx/docs/getting-started.md b/docfx/docs/getting-started.md new file mode 100644 index 000000000..e353bffba --- /dev/null +++ b/docfx/docs/getting-started.md @@ -0,0 +1,8 @@ +# Getting Started + +## Installation + +Consume this library via its NuGet Package. +Click on the badge to find its latest version and the instructions for consuming it that best apply to your project. + +[![NuGet package](https://img.shields.io/nuget/v/Microsoft.VisualStudio.Threading.svg)](https://www.nuget.org/packages/Microsoft.VisualStudio.Threading) diff --git a/doc/library_with_jtf.md b/docfx/docs/library_with_jtf.md similarity index 96% rename from doc/library_with_jtf.md rename to docfx/docs/library_with_jtf.md index c93afe46d..351db71a5 100644 --- a/doc/library_with_jtf.md +++ b/docfx/docs/library_with_jtf.md @@ -115,7 +115,7 @@ public static class LibrarySettings } ``` -This pattern and self-initializer allows all the rest of your library code to assume JTF is always present (so you can use JTF.Run and JTF.RunAsync everywhere w/o feature that JTF will be null), and it mitigates all the deadlocks possible given the host constraints. +This pattern and self-initializer allows all the rest of your library code to assume JTF is always present (so you can use JTF.Run and JTF.RunAsync everywhere w/o fear that JTF will be null), and it mitigates all the deadlocks possible given the host constraints. Note that when you create your own default instance of JoinableTaskContext (i.e. when the host doesn't), it will consider the thread you're on to be the main thread. If SynchronizationContext.Current is object it will capture it and use it to switch to the main thread when you ask it to (very similar to how VS works today), otherwise any request to SwitchToMainThreadAsync will never switch the thread (since no `SynchronizationContext` was supplied to do so) but otherwise JTF continues to work. @@ -124,4 +124,4 @@ Note that when you create your own default instance of JoinableTaskContext (i.e. If your library is app-agnostic (such that it cannot use an app-specific mechanism to obtain an instance of `JoinableTaskContext`) and has no good singleton class on which the app can set the `JoinableTaskContext` instance for the entire library's use, the last option is simply to take a `JoinableTaskContext` instance as a parameter when you need it. For example, [the `AsyncLazy` constructor accepts a `JoinableTaskFactory` as an optional parameter](https://github.com/Microsoft/vs-threading/blob/027bff027c829cab6be54dbd15551d763199ebf0/src/Microsoft.VisualStudio.Threading/AsyncLazy.cs#L60). -When you make the `JoinableTaskContext`/`JoinableTaskFactory` argument optional, the [VSTHRD012](analyzers/VSTHRD012.md) rule can guide your library's users to specify it if they have it available. +When you make the `JoinableTaskContext`/`JoinableTaskFactory` argument optional, the [VSTHRD012](../analyzers/VSTHRD012.md) rule can guide your library's users to specify it if they have it available. diff --git a/doc/testing_vs.md b/docfx/docs/testing_vs.md similarity index 100% rename from doc/testing_vs.md rename to docfx/docs/testing_vs.md diff --git a/doc/threading_rules.md b/docfx/docs/threading_rules.md similarity index 81% rename from doc/threading_rules.md rename to docfx/docs/threading_rules.md index 95a022fcc..cf6115457 100644 --- a/doc/threading_rules.md +++ b/docfx/docs/threading_rules.md @@ -1,7 +1,4 @@ -3 Threading Rules -================= - -## Background +# 3 Threading Rules In Visual Studio 2013, we consolidated all our lessons learned from writing a complex, multi-threaded component of Visual Studio into a small and simple set of @@ -13,11 +10,14 @@ extensions](cookbook_vs.md)). ## The Rules -The rules are listed below with minimal examples. For a more thorough explanation with more examples, check out [this slideshow](https://www.slideshare.net/aarnott/the-3-vs-threading-rules). +The rules are listed below with minimal examples. For a more thorough explanation with more examples, check out [this slideshow](https://www.slideshare.net/slideshow/the-3-vs-threading-rules/78280010). + +### Rule #1. Use `JTF.SwitchToMainThreadAsync` + +If a method has certain thread apartment requirements (STA or MTA) it must either: -### Rule #1. If a method has certain thread apartment requirements (STA or MTA) it must either: 1. Have an asynchronous signature, and asynchronously marshal to the appropriate - thread if it isn't originally invoked on a compatible thread. The recommended + thread if it isn't originally invoked on a compatible thread. The recommended means of switching to the main thread is: ```csharp @@ -25,7 +25,7 @@ The rules are listed below with minimal examples. For a more thorough explanatio ``` OR - + 2. Have a synchronous signature, and throw an exception when called on the wrong thread. This can be done in Visual Studio with `ThreadHelper.ThrowIfNotOnUIThread()` or `ThreadHelper.ThrowIfOnUIThread()`. @@ -33,8 +33,10 @@ The rules are listed below with minimal examples. For a more thorough explanatio In particular, no method is allowed to synchronously marshal work to another thread (blocking while that work is done) except by using the second rule (below). Synchronous blocks in general are to be avoided whenever possible. - -### Rule #2. When an implementation of an already-shipped public API must call asynchronous code and block for its completion, it must do so by following this simple pattern: + +### Rule #2. Use `JTF.Run` + +When an implementation of an already-shipped public API must call asynchronous code and block for its completion, it must do so by following this simple pattern: ```csharp joinableTaskFactoryInstance.Run(async delegate @@ -42,8 +44,10 @@ joinableTaskFactoryInstance.Run(async delegate await SomeOperationAsync(...); }); ``` - -### Rule #3. If ever awaiting work that was started earlier, that work must be *joined*. + +### Rule #3. Use `JTF.RunAsync` + +If ever awaiting work that was started earlier, that work must be *joined*. For example, one service kicks off some asynchronous work that may later become synchronously blocking: @@ -57,39 +61,41 @@ JoinableTask longRunningAsyncWork = joinableTaskFactoryInstance.RunAsync( then later that async work becomes blocking: -```csharp +```csharp longRunningAsyncWork.Join(); ``` -or perhaps +or perhaps -```csharp +```csharp await longRunningAsyncWork; ``` Note however that this extra step is not necessary when awaiting is done immediately after kicking off an asynchronous operation. - -In particular, no method should call `Task.Wait()` or `Task.Result` on + +In particular, no method should call `Task.Wait()` or `Task.Result` on an incomplete `Task`. - -### Additional "honorable mention" rules: (Not JTF related) - -### Rule #4. Never define `async void` methods. Make the methods return `Task` instead. - - Exceptions thrown from `async void` methods always crash the process. - - Callers don't even have the option to `await` the result. - - Exceptions can't be reported to telemetry by the caller. - - It's impossible for your VS package to responsibly block in `Package.Close` - till your `async` work is done when it was kicked off this way. - - Be cautious: `async delegate` or `async () =>` become `async void` - methods when passed to a method that accepts `Action` delegates. Only - pass `async` delegates to methods that accept `Func` or - `Func>` parameters. - -Frequently Asked Questions ---------------- - -##### Do I need to follow these rules? + +### Rule #4. Avoid `async void` + +(This one is more of an "honorable mention" rule, as it is not JTF related.) + +Never define `async void` methods. Make the methods return `Task` instead. + +- Exceptions thrown from `async void` methods always crash the process. +- Callers don't even have the option to `await` the result. +- Exceptions can't be reported to telemetry by the caller. +- It's impossible for your VS package to responsibly block in `Package.Close` + till your `async` work is done when it was kicked off this way. +- Be cautious: `async delegate` or `async () =>` become `async void` + methods when passed to a method that accepts `Action` delegates. Only + pass `async` delegates to methods that accept `Func` or + `Func>` parameters. + +## Frequently Asked Questions + +### Do I need to follow these rules? All code that runs in Visual Studio itself should follow these rules. These rules have been reviewed by several senior and principal developers @@ -99,7 +105,7 @@ would do well to follow them in managed code where possible. Any other GUI app that invokes asynchronous code that it must occasionally block the UI thread on is also recommended to follow these rules. -##### Why should a method that has a dependency on a specific (kind of) thread be async? +### Why should a method that has a dependency on a specific (kind of) thread be async? Efficiency and responsiveness: Switching threads means that the original thread either can do something else productive (e.g., execute more work from @@ -115,7 +121,7 @@ requires, whether it's thread-safe, etc. The implementation can change over time to add or remove thread affinity, or to switch from locking to scheduling for thread safety, etc. -##### Why do I need to use `JoinableTaskFactory.Run` to synchronously block on asynchronous work rather than just calling `Task.Wait()` or `Task.Result`? +### Why do I need to use `JoinableTaskFactory.Run` to synchronously block on asynchronous work rather than just calling `Task.Wait()` or `Task.Result`? If you're on the main thread, because `Task.Wait` or `Task.Result` will often deadlock because you're now synchronously blocking the main thread for @@ -135,72 +141,72 @@ In contrast, when you use `JoinableTaskFactory.Run`, main thread deadlocks and threadpool exhaustion are automatically mitigated by reusing the blocking thread to execute the continuations. -##### Why not rely on COM marshaling to switch to the main thread when necessary? +### Why not rely on COM marshaling to switch to the main thread when necessary? There are several reasons for this: -1. The COM transition synchronously blocks the calling thread. If the - main thread isn't immediately pumping messages, the MTA thread will - block until it handles the message. If you're on a threadpool thread, - this ties up a precious resource and if your code may execute on - multiple threadpool threads at once, there is a very real possibility +1. The COM transition synchronously blocks the calling thread. If the + main thread isn't immediately pumping messages, the MTA thread will + block until it handles the message. If you're on a threadpool thread, + this ties up a precious resource and if your code may execute on + multiple threadpool threads at once, there is a very real possibility of [threadpool starvation](threadpool_starvation.md). -2. Deadlock: if the main thread is blocked waiting for the background - thread, and the main thread happens to be on top of some call stack - (like WPF measure-layout) that suppresses the message pump, the code +2. Deadlock: if the main thread is blocked waiting for the background + thread, and the main thread happens to be on top of some call stack + (like WPF measure-layout) that suppresses the message pump, the code that normally works will randomly deadlock. -3. When the main thread is pumping messages, it will execute your code, - regardless as to whether it is relevant to what the main thread may - already be doing. If the main thread is in the main message pump, - that's fine. But if the main thread is in a pumping wait (in - managed code this could be almost anywhere as this includes locks, - I/O, sync blocks, etc.) it could be a very bad time. We call these - bad times "reentrancy" and the problem comes when you have component - X running on the main thread in a pumping wait, the component Y - uses COM marshalling to re-enter the main thread, and then Y calls - (directly or indirectly) into component X. Component X is typically - written with the assumption that by being on the main thread, it's - isolated and single-threaded, and it usually isn't prepared to handle - reentrancy. As a result, data corruption and/or deadlocks can result. - Such has been the source of many deadlocks and crashes in VS for the +3. When the main thread is pumping messages, it will execute your code, + regardless as to whether it is relevant to what the main thread may + already be doing. If the main thread is in the main message pump, + that's fine. But if the main thread is in a pumping wait (in + managed code this could be almost anywhere as this includes locks, + I/O, sync blocks, etc.) it could be a very bad time. We call these + bad times "reentrancy" and the problem comes when you have component + X running on the main thread in a pumping wait, the component Y + uses COM marshalling to re-enter the main thread, and then Y calls + (directly or indirectly) into component X. Component X is typically + written with the assumption that by being on the main thread, it's + isolated and single-threaded, and it usually isn't prepared to handle + reentrancy. As a result, data corruption and/or deadlocks can result. + Such has been the source of many deadlocks and crashes in VS for the last few releases. -4. Any method from a VS service that returns a pointer is probably - inherently broken when called from a background thread. For example, - `ItemID`s returned from `IVsHierarchy` are very often raw pointers cast - to integers. These pointers are guaranteed to be valid for as long - as you're on the main thread (and no event was raised to invalidate - it). But when you call a `IVsHierarchy` method to get an `ItemID` back - from a background thread, you leave the STA thread immediately as - the call returns, meaning the pointer is unsafe to use. If you then - go and pass that pointer back into the project system, the pointer - could have been invalidated in the interim, and you'll end up causing - an access violation crash in VS. The only safe way to deal with - `ItemID`s (or any other pointer type) is while manually marshaled to - the UI thread so that you know they are still valid for as long as +4. Any method from a VS service that returns a pointer is probably + inherently broken when called from a background thread. For example, + `ItemID`s returned from `IVsHierarchy` are very often raw pointers cast + to integers. These pointers are guaranteed to be valid for as long + as you're on the main thread (and no event was raised to invalidate + it). But when you call a `IVsHierarchy` method to get an `ItemID` back + from a background thread, you leave the STA thread immediately as + the call returns, meaning the pointer is unsafe to use. If you then + go and pass that pointer back into the project system, the pointer + could have been invalidated in the interim, and you'll end up causing + an access violation crash in VS. The only safe way to deal with + `ItemID`s (or any other pointer type) is while manually marshaled to + the UI thread so that you know they are still valid for as long as you hold and use them. -5. If your method runs on a background thread and has a loop that - accesses a VS service, that can incur a lot of thread transitions - which can hurt performance. If you were explicit in your code about - the transition, you'd very likely move it to just before you enter +5. If your method runs on a background thread and has a loop that + accesses a VS service, that can incur a lot of thread transitions + which can hurt performance. If you were explicit in your code about + the transition, you'd very likely move it to just before you enter the loop, which would make your code more efficient from the start. -6. Some VS services don't have proxy stubs registered and thus will fail - to the type cast or on method invocation when your code executes on +6. Some VS services don't have proxy stubs registered and thus will fail + to the type cast or on method invocation when your code executes on a background thread. -7. Some VS services get rewritten from native to managed code, which - subtly changes them from single-threaded to free-threaded services. - Unless the managed code is written to be thread-safe (most is not) - this means that your managed code calling into a managed code VS - service on a background thread will not transition to the UI thread - first, and you are cruising for thread-safety bugs (data corruption, - crashes, hangs, etc). By switching to the main thread yourself first, - you won't be the poor soul who has crashes in their feature and has - to debug it for days until you finally figure out that you were causing - data corruption and a crash later on. Yes, you can blame the free - threaded managed code that should have protected itself, but that's - not very satisfying after days of investigation. And the owner of +7. Some VS services get rewritten from native to managed code, which + subtly changes them from single-threaded to free-threaded services. + Unless the managed code is written to be thread-safe (most is not) + this means that your managed code calling into a managed code VS + service on a background thread will not transition to the UI thread + first, and you are cruising for thread-safety bugs (data corruption, + crashes, hangs, etc). By switching to the main thread yourself first, + you won't be the poor soul who has crashes in their feature and has + to debug it for days until you finally figure out that you were causing + data corruption and a crash later on. Yes, you can blame the free + threaded managed code that should have protected itself, but that's + not very satisfying after days of investigation. And the owner of that code may refuse to fix their code and you'll have to fix yours anyway. -##### How do these rules protect me from re-entering random code on the main thread? +### How do these rules protect me from re-entering random code on the main thread? By always using asynchronous mechanisms to marshal to the UI thread, you're effectively send a `PostMessage` to the UI thread, which will not re-enter @@ -218,7 +224,7 @@ main thread. That is, using this method to get to the UI thread just works: it avoids both deadlocks and undesirable reentrancy. The only time it deadlocks is when the threading rules listed above are not being followed. -##### Am I protected from other code re-entering my own code while it executes on the main thread? +### Am I protected from other code re-entering my own code while it executes on the main thread? Yes, somewhat. When you call `JoinableTaskFactory.Run` with an async delegate, when your delegate yields (using await) the message pump is temporarily @@ -236,25 +242,25 @@ disabling the message pump yourself, it's usually not a good idea because 3rd party code you may be calling could be relying on a functioning message pump. -##### I'm trying to analyze a hang around code that uses `JoinableTaskFactory`, but since transitions are asynchronous the active threads' call stacks don't tell the whole story. How can I find the cause and fix the hang? +### I'm trying to analyze a hang around code that uses `JoinableTaskFactory`, but since transitions are asynchronous the active threads' call stacks don't tell the whole story. How can I find the cause and fix the hang? Debugging async hangs in general is lacking debugger tooling support at the moment. The debugger and Windows teams are working to improve that situation. In the meantime, we have learned several techniques to figure out what is causing the hang, and we're working to enhance the framework to automatically detect, self-analyze and report hangs to you so you have -almost nothing to do but fix the code bug. +almost nothing to do but fix the code bug. In the meantime, the most useful technique for analyzing async hangs is to attach WinDBG to the process and dump out incomplete async methods' states. This can be tedious, but we have a script in this file that you can use to make it much easier: [Async hang debugging][AsyncHangDebugging] -##### What is threadpool exhaustion, and why is it bad? +### What is threadpool exhaustion, and why is it bad? See our [threadpool starvation](threadpool_starvation.md) doc. -##### I'm writing an async method that isn't in a `JoinableTask`. Should I use `JTF.SwitchToMainThreadAsync()` to get to the UI thread? +### I'm writing an async method that isn't in a `JoinableTask`. Should I use `JTF.SwitchToMainThreadAsync()` to get to the UI thread? Yes. `JoinableTaskFactory.SwitchToMainThreadAsync()` works great outside a `JoinableTask`. It simply posts the continuation to the main thread for @@ -281,13 +287,13 @@ focus on the fact that at this point, your code needs the main thread and call `JTF.SwitchToMainThreadAsync()`. This allows your caller to set the priority via the `JoinableTask` it may call your code within. -##### What message priority is used to switch to (or resume on) the main thread, and can this be changed? +### What message priority is used to switch to (or resume on) the main thread, and can this be changed? -`JoinableTaskFactory`’s default behavior is to switch to the main thread using +`JoinableTaskFactory`'s default behavior is to switch to the main thread using `SynchronizationContext.Post`, which typically posts a message to the main thread, which puts it below RPC and above user input in priority. -[How to use a different priority for switching to the main thread in VS](cookbook_vs.md#how-to-switch-to-or-use-the-ui-thread-with-background-priority) +[How to use a different priority for switching to the main thread in VS](cookbook_vs.md#how-to-switch-to-or-use-the-ui-thread-with-a-specific-priority) The following describes how to replace the mechanism for getting to the UI thread in a host-independent way: @@ -303,8 +309,8 @@ your own constructor that chains in the base constructor, passing in the required parameters. You are then free to directly instantiate your derived type by passing in either a `JoinableTaskContext` or a `JoinableTaskCollection`. -For more information on this topic, see Andrew Arnott's blog post -[Asynchronous and multithreaded programming within VS using the +For more information on this topic, see Andrew Arnott's blog post +[Asynchronous and multithreaded programming within VS using the `JoinableTaskFactory`][JTFBlog]. [AsyncHangDebugging]: https://github.com/Microsoft/VSProjectSystem/blob/master/doc/scenario/analyze_hangs.md diff --git a/doc/threadpool_starvation.md b/docfx/docs/threadpool_starvation.md similarity index 90% rename from doc/threadpool_starvation.md rename to docfx/docs/threadpool_starvation.md index 8162c5e84..f8ac0db06 100644 --- a/doc/threadpool_starvation.md +++ b/docfx/docs/threadpool_starvation.md @@ -31,7 +31,7 @@ We use [PerfView](https://aka.ms/perfview) for these investigations. First, consider how we might come to suspect that thread pool starvation is to blame for a performance or responsiveness problem in the application. In PerfView if we were looking at a sluggish scenario with the CPU Stacks window, we might observe this: -![PerfView CPU Stacks view showing large columns of no CPU activity](images/cpu_stacks_showing_threadpool_starvation.png) +![PerfView CPU Stacks view showing large columns of no CPU activity](../images/cpu_stacks_showing_threadpool_starvation.png) Notice how the `When` column shows several vertical columns of time where there is little or no CPU activity. This is a good indication that we have either excessive lock contention or thread pool exhaustion. @@ -39,7 +39,7 @@ Notice how the `When` column shows several vertical columns of time where there Recent versions of Visual Studio raise an ETW event called `Microsoft-VisualStudio-Common/vs_core_perf_threadpoolstarvation` when thread pool starvation is detected. This is a sure clue of the problem and can give you a time range within the trace to focus your investigation. -![PerfView showing the VS ETW event that indicates thread pool starvation](images/vs_threadpoolstarvation_event.jpg) +![PerfView showing the VS ETW event that indicates thread pool starvation](../images/vs_threadpoolstarvation_event.jpg) ### Investigation steps @@ -49,7 +49,7 @@ While the CLR has thread pool ETW events to indicate thread starvation, these ev 1. In the Thread Time Stacks window, set the Start and End fields to the time range where you had a responsiveness problem. 1. Make sure symbols for the `clr` module are loaded. 1. In the "By Name" tab, find the `clr!ThreadpoolMgr::ExecuteWorkRequest` frame and invoke the "Include Items" command. This will add the frame to the `IncPats` field and filter all frames and stacks to those found on threadpool threads. -1. Also in the "By Name" tab, find the `BLOCKED_TIME` row and invoke the "Show Callers" command. ![PerfView By Name tab showing BLOCKED_TIME](images/blocked_time.png) This will show all stacks that led to any thread pool thread waiting instead of executing on the CPU. ![PerfView Callers of BLOCKED_TIME](images/blocked_time_callers.png) +1. Also in the "By Name" tab, find the `BLOCKED_TIME` row and invoke the "Show Callers" command. ![PerfView By Name tab showing BLOCKED_TIME](../images/blocked_time.png) This will show all stacks that led to any thread pool thread waiting instead of executing on the CPU. ![PerfView Callers of BLOCKED_TIME](../images/blocked_time_callers.png) Take a look at the stacks where the most threads or the most time is spent blocked. This is the code where you should focus your effort to remove the synchronous block. Common mitigations include: @@ -73,7 +73,7 @@ There are multiple major causes of thread pool starvation. Each is briefly descr ### Blocking a thread pool thread while waiting for the UI thread -When a thread pool thread tries to access an STA COM object such as Visual Studio's IServiceProvider or a service previously obtained from this interface, the call to that COM object will require an RPC (Remote Procedure Call) transition which blocks the thread pool thread until the UI thread has time to respond to the request. Learn more about RPC calls from [this blog post](https://blogs.msdn.microsoft.com/andrewarnottms/2014/05/07/asynchronous-and-multithreaded-programming-within-vs-using-the-joinabletaskfactory/). +When a thread pool thread tries to access an STA COM object such as Visual Studio's IServiceProvider or a service previously obtained from this interface, the call to that COM object will require an RPC (Remote Procedure Call) transition which blocks the thread pool thread until the UI thread has time to respond to the request. Learn more about RPC calls from [this blog post](https://devblogs.microsoft.com/premier-developer/asynchronous-and-multithreaded-programming-within-vs-using-the-joinabletaskfactory/). The mitigation for this is to have the method that is executing on the thread pool asynchronously switch to the UI thread *before* calling into an STA COM object. This allows the thread pool thread to work on something else on the thread pool's queue while the UI thread is busy or servicing this request. After interacting with the STA COM object, the async method can switch back to the thread pool if desired. @@ -81,8 +81,8 @@ The mitigation for this is to have the method that is executing on the thread po When a component sends many work items to the thread pool in a short timeframe, the queue will grow to store them till one of the thread pool threads can execute them all. Any subsequently queued items will be added to the end of the queue, regardless of their relative priority in the application. When the queue is long, and a work item is appended to the end of the queue that is required for the UI of the application to feel responsive, the application can hang or feel sluggish due to the work that otherwise should be running in the background without impacting UI responsiveness. -The mitigation is for components that have many work items to send to the threadpool to throttle the rate at which new items are introduced to the threadpool to a reasonably small number. This helps keep the queue short, and thus any newly enqueued work will execute much sooner, keeping the application responsive. Throttling work can be done such that the CPU stays busy and the background work moving along quickly, but without sacrificing UI responsiveness. See [this blog post](https://blogs.msdn.microsoft.com/andrewarnottms/2017/05/11/limiting-concurrency-for-faster-and-more-responsive-apps/) for more information on how to easily throttle concurrent work. +The mitigation is for components that have many work items to send to the threadpool to throttle the rate at which new items are introduced to the threadpool to a reasonably small number. This helps keep the queue short, and thus any newly enqueued work will execute much sooner, keeping the application responsive. Throttling work can be done such that the CPU stays busy and the background work moving along quickly, but without sacrificing UI responsiveness. See [this blog post](https://devblogs.microsoft.com/premier-developer/limiting-concurrency-for-faster-and-more-responsive-apps/) for more information on how to easily throttle concurrent work. ## Learn more -Vance Morrison wrote [a blog post](https://blogs.msdn.microsoft.com/vancem/2018/10/16/diagnosing-net-core-threadpool-starvation-with-perfview-why-my-service-is-not-saturating-all-cores-or-seems-to-stall/) describing this situation as well. +Vance Morrison wrote [a blog post](https://learn.microsoft.com/archive/blogs/vancem/diagnosing-net-core-threadpool-starvation-with-perfview-why-my-service-is-not-saturating-all-cores-or-seems-to-stall) describing this situation as well. diff --git a/docfx/docs/toc.yml b/docfx/docs/toc.yml new file mode 100644 index 000000000..0e6eb0e07 --- /dev/null +++ b/docfx/docs/toc.yml @@ -0,0 +1,10 @@ +items: +- href: getting-started.md +- href: features.md +- href: threading_rules.md +- href: cookbook_vs.md +- href: testing_vs.md +- href: library_with_jtf.md +- href: threadpool_starvation.md +- href: async_hang.md +- href: dumpasync.md diff --git a/doc/images/blocked_time.png b/docfx/images/blocked_time.png similarity index 100% rename from doc/images/blocked_time.png rename to docfx/images/blocked_time.png diff --git a/doc/images/blocked_time_callers.png b/docfx/images/blocked_time_callers.png similarity index 100% rename from doc/images/blocked_time_callers.png rename to docfx/images/blocked_time_callers.png diff --git a/doc/images/cpu_stacks_showing_threadpool_starvation.png b/docfx/images/cpu_stacks_showing_threadpool_starvation.png similarity index 100% rename from doc/images/cpu_stacks_showing_threadpool_starvation.png rename to docfx/images/cpu_stacks_showing_threadpool_starvation.png diff --git a/doc/analyzers/images/fsa.png b/docfx/images/fsa.png similarity index 100% rename from doc/analyzers/images/fsa.png rename to docfx/images/fsa.png diff --git a/doc/images/vs_threadpoolstarvation_event.jpg b/docfx/images/vs_threadpoolstarvation_event.jpg similarity index 100% rename from doc/images/vs_threadpoolstarvation_event.jpg rename to docfx/images/vs_threadpoolstarvation_event.jpg diff --git a/docfx/index.md b/docfx/index.md new file mode 100644 index 000000000..637535dda --- /dev/null +++ b/docfx/index.md @@ -0,0 +1,10 @@ +--- +_layout: landing +--- + +# Overview + +This is the documentation site for the Microsoft.VisualStudio.Threading library and analyzers. + +Click the Docs or Analyzers heading on the top to read the docs. +Or click the API heading to see API-level documentation. diff --git a/docfx/toc.yml b/docfx/toc.yml new file mode 100644 index 000000000..cf4bdcb5a --- /dev/null +++ b/docfx/toc.yml @@ -0,0 +1,9 @@ +items: +- name: Docs + href: docs/ +- name: Analyzers + href: analyzers/ +- name: API + href: api/ +- name: GitHub + href: https://github.com/microsoft/vs-threading diff --git a/global.json b/global.json index e514f3f76..40ed7211f 100644 --- a/global.json +++ b/global.json @@ -1,10 +1,14 @@ { "sdk": { - "version": "6.0.100", + "version": "10.0.302", "rollForward": "patch", "allowPrerelease": false }, + "test": { + "runner": "Microsoft.Testing.Platform" + }, "msbuild-sdks": { - "MSBuild.Sdk.Extras": "3.0.44" + "Microsoft.Build.NoTargets": "3.7.134", + "Microsoft.Build.Traversal": "4.1.82" } } diff --git a/init.ps1 b/init.ps1 old mode 100644 new mode 100755 index 8374dd565..183a7156d --- a/init.ps1 +++ b/init.ps1 @@ -28,6 +28,8 @@ No effect if -NoPrerequisites is specified. .PARAMETER NoRestore Skips the package restore step. +.PARAMETER NoToolRestore + Skips the dotnet tool restore step. .PARAMETER Signing Install the MicroBuild signing plugin for building test-signed builds on desktop machines. .PARAMETER Localization @@ -37,8 +39,12 @@ when building. .PARAMETER OptProf Install the MicroBuild OptProf plugin for building optimized assemblies on desktop machines. +.PARAMETER Sbom + Install the MicroBuild SBOM plugin. .PARAMETER AccessToken An optional access token for authenticating to Azure Artifacts authenticated feeds. +.PARAMETER Interactive + Runs NuGet restore in interactive mode. This can turn authentication failures into authentication challenges. #> [CmdletBinding(SupportsShouldProcess = $true)] Param ( @@ -53,13 +59,19 @@ Param ( [Parameter()] [switch]$NoRestore, [Parameter()] + [switch]$NoToolRestore, + [Parameter()] [switch]$Signing, [Parameter()] [switch]$Localization, [Parameter()] [switch]$OptProf, [Parameter()] - [string]$AccessToken + [switch]$SBOM, + [Parameter()] + [string]$AccessToken, + [Parameter()] + [switch]$Interactive ) $EnvVars = @{} @@ -78,7 +90,7 @@ if (!$NoPrerequisites) { # The procdump tool and env var is required for dotnet test to collect hang/crash dumps of tests. # But it only works on Windows. if ($env:OS -eq 'Windows_NT') { - $EnvVars['PROCDUMP_PATH'] = & "$PSScriptRoot\azure-pipelines\Get-ProcDump.ps1" + $EnvVars['PROCDUMP_PATH'] = & "$PSScriptRoot\tools\Get-ProcDump.ps1" } } @@ -90,15 +102,27 @@ Push-Location $PSScriptRoot try { $HeaderColor = 'Green' + $RestoreArguments = @() + if ($Interactive) { + $RestoreArguments += '--interactive' + } + if (!$NoRestore -and $PSCmdlet.ShouldProcess("NuGet packages", "Restore")) { Write-Host "Restoring NuGet packages" -ForegroundColor $HeaderColor - dotnet restore + dotnet restore @RestoreArguments if ($lastexitcode -ne 0) { throw "Failure while restoring packages." } } - $InstallNuGetPkgScriptPath = ".\azure-pipelines\Install-NuGetPackage.ps1" + if (!$NoToolRestore -and $PSCmdlet.ShouldProcess("dotnet tool", "restore")) { + dotnet tool restore @RestoreArguments + if ($lastexitcode -ne 0) { + throw "Failure while restoring dotnet CLI tools." + } + } + + $InstallNuGetPkgScriptPath = "$PSScriptRoot\tools\Install-NuGetPackage.ps1" $nugetVerbosity = 'quiet' if ($Verbose) { $nugetVerbosity = 'normal' } $MicroBuildPackageSource = 'https://pkgs.dev.azure.com/devdiv/_packaging/MicroBuildToolset%40Local/nuget/v3/index.json' @@ -121,6 +145,16 @@ try { $EnvVars['LocLanguages'] = "JPN" } + if ($SBOM) { + Write-Host "Installing MicroBuild SBOM plugin" -ForegroundColor $HeaderColor + & $InstallNuGetPkgScriptPath MicroBuild.Plugins.Sbom -source $MicroBuildPackageSource -Verbosity $nugetVerbosity + # The feed with the latest versions of the tool is at 'https://1essharedassets.pkgs.visualstudio.com/1esPkgs/_packaging/SBOMTool/nuget/v3/index.json', + # but we'll use the feed that the SBOM task itself uses to install the tool for consistency. + $PkgMicrosoft_ManifestTool_CrossPlatform = & $InstallNuGetPkgScriptPath Microsoft.ManifestTool.CrossPlatform -source $MicroBuildPackageSource -Verbosity $nugetVerbosity + $EnvVars['GenerateSBOM'] = "true" + $EnvVars['PkgMicrosoft_ManifestTool_CrossPlatform'] = $PkgMicrosoft_ManifestTool_CrossPlatform + } + & "$PSScriptRoot/tools/Set-EnvVars.ps1" -Variables $EnvVars -PrependPath $PrependPath | Out-Null } catch { diff --git a/loc/lci/Microsoft.VisualStudio.Threading.Analyzers.dll.lci b/loc/lci/Microsoft.VisualStudio.Threading.Analyzers.dll.lci new file mode 100644 index 000000000..de341924d --- /dev/null +++ b/loc/lci/Microsoft.VisualStudio.Threading.Analyzers.dll.lci @@ -0,0 +1,7 @@ + + + + + + + diff --git a/loc/lci/Microsoft.VisualStudio.Threading.dll.lci b/loc/lci/Microsoft.VisualStudio.Threading.dll.lci new file mode 100644 index 000000000..4b271fc27 --- /dev/null +++ b/loc/lci/Microsoft.VisualStudio.Threading.dll.lci @@ -0,0 +1,7 @@ + + + + + + + diff --git a/loc/lci/SosThreadingToolsManaged.dll.lci b/loc/lci/SosThreadingToolsManaged.dll.lci new file mode 100644 index 000000000..3c773f4de --- /dev/null +++ b/loc/lci/SosThreadingToolsManaged.dll.lci @@ -0,0 +1,7 @@ + + + + + + + diff --git a/loc/lcl/CHS/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl b/loc/lcl/CHS/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl index d29692d15..ca1b57be4 100644 --- a/loc/lcl/CHS/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl +++ b/loc/lcl/CHS/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl @@ -3,6 +3,7 @@ + @@ -432,10 +433,13 @@ - + - + + + + @@ -577,6 +581,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/loc/lcl/CHS/Microsoft.VisualStudio.Threading.dll.lcl b/loc/lcl/CHS/Microsoft.VisualStudio.Threading.dll.lcl index ac93c5c50..e4dec9e54 100644 --- a/loc/lcl/CHS/Microsoft.VisualStudio.Threading.dll.lcl +++ b/loc/lcl/CHS/Microsoft.VisualStudio.Threading.dll.lcl @@ -3,6 +3,7 @@ + @@ -130,6 +131,15 @@ + + + + + + + + + @@ -244,10 +254,13 @@ - + - + + + + diff --git a/loc/lcl/CHS/SosThreadingToolsManaged.dll.lcl b/loc/lcl/CHS/SosThreadingToolsManaged.dll.lcl new file mode 100644 index 000000000..906fd606c --- /dev/null +++ b/loc/lcl/CHS/SosThreadingToolsManaged.dll.lcl @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/loc/lcl/CHT/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl b/loc/lcl/CHT/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl index 4b94fba62..533189337 100644 --- a/loc/lcl/CHT/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl +++ b/loc/lcl/CHT/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl @@ -3,6 +3,7 @@ + @@ -432,10 +433,13 @@ - + - + + + + @@ -577,6 +581,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/loc/lcl/CHT/Microsoft.VisualStudio.Threading.dll.lcl b/loc/lcl/CHT/Microsoft.VisualStudio.Threading.dll.lcl index 46e8b57ed..d63dff3e6 100644 --- a/loc/lcl/CHT/Microsoft.VisualStudio.Threading.dll.lcl +++ b/loc/lcl/CHT/Microsoft.VisualStudio.Threading.dll.lcl @@ -3,6 +3,7 @@ + @@ -130,6 +131,15 @@ + + + + + + + + + @@ -244,10 +254,13 @@ - + - + + + + diff --git a/loc/lcl/CHT/SosThreadingToolsManaged.dll.lcl b/loc/lcl/CHT/SosThreadingToolsManaged.dll.lcl new file mode 100644 index 000000000..fdb15b841 --- /dev/null +++ b/loc/lcl/CHT/SosThreadingToolsManaged.dll.lcl @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/loc/lcl/CSY/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl b/loc/lcl/CSY/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl index f9169a1d8..8536a06d4 100644 --- a/loc/lcl/CSY/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl +++ b/loc/lcl/CSY/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl @@ -3,6 +3,7 @@ + @@ -432,10 +433,13 @@ - + + + + @@ -577,6 +581,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/loc/lcl/CSY/Microsoft.VisualStudio.Threading.dll.lcl b/loc/lcl/CSY/Microsoft.VisualStudio.Threading.dll.lcl index 3f6bee444..20da45a4e 100644 --- a/loc/lcl/CSY/Microsoft.VisualStudio.Threading.dll.lcl +++ b/loc/lcl/CSY/Microsoft.VisualStudio.Threading.dll.lcl @@ -3,6 +3,7 @@ + @@ -130,6 +131,15 @@ + + + + + + + + + @@ -244,10 +254,13 @@ - + - + + + + diff --git a/loc/lcl/CSY/SosThreadingToolsManaged.dll.lcl b/loc/lcl/CSY/SosThreadingToolsManaged.dll.lcl new file mode 100644 index 000000000..ff26e55cb --- /dev/null +++ b/loc/lcl/CSY/SosThreadingToolsManaged.dll.lcl @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/loc/lcl/DEU/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl b/loc/lcl/DEU/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl index 030eeb5d4..43c5fa90e 100644 --- a/loc/lcl/DEU/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl +++ b/loc/lcl/DEU/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl @@ -3,6 +3,7 @@ + @@ -432,10 +433,13 @@ - + - + + + + @@ -577,6 +581,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/loc/lcl/DEU/Microsoft.VisualStudio.Threading.dll.lcl b/loc/lcl/DEU/Microsoft.VisualStudio.Threading.dll.lcl index 16cd9e1ef..67f888530 100644 --- a/loc/lcl/DEU/Microsoft.VisualStudio.Threading.dll.lcl +++ b/loc/lcl/DEU/Microsoft.VisualStudio.Threading.dll.lcl @@ -3,6 +3,7 @@ + @@ -130,6 +131,15 @@ + + + + + + + + + @@ -244,10 +254,13 @@ - + - + + + + diff --git a/loc/lcl/DEU/SosThreadingToolsManaged.dll.lcl b/loc/lcl/DEU/SosThreadingToolsManaged.dll.lcl new file mode 100644 index 000000000..72a557999 --- /dev/null +++ b/loc/lcl/DEU/SosThreadingToolsManaged.dll.lcl @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/loc/lcl/ESN/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl b/loc/lcl/ESN/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl index 1296ec295..bb2833f3f 100644 --- a/loc/lcl/ESN/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl +++ b/loc/lcl/ESN/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl @@ -3,6 +3,7 @@ + @@ -432,10 +433,13 @@ - + + + + @@ -577,6 +581,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/loc/lcl/ESN/Microsoft.VisualStudio.Threading.dll.lcl b/loc/lcl/ESN/Microsoft.VisualStudio.Threading.dll.lcl index bc0cee074..154ae0b95 100644 --- a/loc/lcl/ESN/Microsoft.VisualStudio.Threading.dll.lcl +++ b/loc/lcl/ESN/Microsoft.VisualStudio.Threading.dll.lcl @@ -3,6 +3,7 @@ + @@ -130,6 +131,15 @@ + + + + + + + + + @@ -244,10 +254,13 @@ - + - + + + + diff --git a/loc/lcl/ESN/SosThreadingToolsManaged.dll.lcl b/loc/lcl/ESN/SosThreadingToolsManaged.dll.lcl new file mode 100644 index 000000000..4d5da5f78 --- /dev/null +++ b/loc/lcl/ESN/SosThreadingToolsManaged.dll.lcl @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/loc/lcl/FRA/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl b/loc/lcl/FRA/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl index 313a5d809..4a46cf116 100644 --- a/loc/lcl/FRA/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl +++ b/loc/lcl/FRA/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl @@ -3,6 +3,7 @@ + @@ -432,10 +433,13 @@ - + - + + + + @@ -577,6 +581,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/loc/lcl/FRA/Microsoft.VisualStudio.Threading.dll.lcl b/loc/lcl/FRA/Microsoft.VisualStudio.Threading.dll.lcl index 047eb98af..541874b3a 100644 --- a/loc/lcl/FRA/Microsoft.VisualStudio.Threading.dll.lcl +++ b/loc/lcl/FRA/Microsoft.VisualStudio.Threading.dll.lcl @@ -3,6 +3,7 @@ + @@ -130,6 +131,15 @@ + + + + + + + + + @@ -244,10 +254,13 @@ - + - + + + + diff --git a/loc/lcl/FRA/SosThreadingToolsManaged.dll.lcl b/loc/lcl/FRA/SosThreadingToolsManaged.dll.lcl new file mode 100644 index 000000000..1a1787810 --- /dev/null +++ b/loc/lcl/FRA/SosThreadingToolsManaged.dll.lcl @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/loc/lcl/ITA/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl b/loc/lcl/ITA/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl index bfca7dddc..3c28ee9b5 100644 --- a/loc/lcl/ITA/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl +++ b/loc/lcl/ITA/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl @@ -3,6 +3,7 @@ + @@ -432,10 +433,13 @@ - + - + + + + @@ -577,6 +581,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/loc/lcl/ITA/Microsoft.VisualStudio.Threading.dll.lcl b/loc/lcl/ITA/Microsoft.VisualStudio.Threading.dll.lcl index c07887dbf..2fce11fe6 100644 --- a/loc/lcl/ITA/Microsoft.VisualStudio.Threading.dll.lcl +++ b/loc/lcl/ITA/Microsoft.VisualStudio.Threading.dll.lcl @@ -3,6 +3,7 @@ + @@ -130,6 +131,15 @@ + + + + + + + + + @@ -244,10 +254,13 @@ - + - + + + + diff --git a/loc/lcl/ITA/SosThreadingToolsManaged.dll.lcl b/loc/lcl/ITA/SosThreadingToolsManaged.dll.lcl new file mode 100644 index 000000000..9f30d6b9c --- /dev/null +++ b/loc/lcl/ITA/SosThreadingToolsManaged.dll.lcl @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/loc/lcl/JPN/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl b/loc/lcl/JPN/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl index 883c12231..56b89ea38 100644 --- a/loc/lcl/JPN/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl +++ b/loc/lcl/JPN/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl @@ -3,6 +3,7 @@ + @@ -432,10 +433,13 @@ - + - + + + + @@ -577,6 +581,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/loc/lcl/JPN/Microsoft.VisualStudio.Threading.dll.lcl b/loc/lcl/JPN/Microsoft.VisualStudio.Threading.dll.lcl index f234ff8ff..dbfc1d77a 100644 --- a/loc/lcl/JPN/Microsoft.VisualStudio.Threading.dll.lcl +++ b/loc/lcl/JPN/Microsoft.VisualStudio.Threading.dll.lcl @@ -3,6 +3,7 @@ + @@ -130,6 +131,15 @@ + + + + + + + + + @@ -244,10 +254,13 @@ - + - + + + + diff --git a/loc/lcl/JPN/SosThreadingToolsManaged.dll.lcl b/loc/lcl/JPN/SosThreadingToolsManaged.dll.lcl new file mode 100644 index 000000000..ef2ddcf08 --- /dev/null +++ b/loc/lcl/JPN/SosThreadingToolsManaged.dll.lcl @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/loc/lcl/KOR/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl b/loc/lcl/KOR/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl index d1f557eb9..36cffb185 100644 --- a/loc/lcl/KOR/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl +++ b/loc/lcl/KOR/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl @@ -3,6 +3,7 @@ + @@ -432,10 +433,13 @@ - + - + + + + @@ -577,6 +581,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/loc/lcl/KOR/Microsoft.VisualStudio.Threading.dll.lcl b/loc/lcl/KOR/Microsoft.VisualStudio.Threading.dll.lcl index 682523e2f..0e1fd43ee 100644 --- a/loc/lcl/KOR/Microsoft.VisualStudio.Threading.dll.lcl +++ b/loc/lcl/KOR/Microsoft.VisualStudio.Threading.dll.lcl @@ -3,6 +3,7 @@ + @@ -130,6 +131,15 @@ + + + + + + + + + @@ -244,10 +254,13 @@ - + - + + + + diff --git a/loc/lcl/KOR/SosThreadingToolsManaged.dll.lcl b/loc/lcl/KOR/SosThreadingToolsManaged.dll.lcl new file mode 100644 index 000000000..277ba25f7 --- /dev/null +++ b/loc/lcl/KOR/SosThreadingToolsManaged.dll.lcl @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/loc/lcl/PLK/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl b/loc/lcl/PLK/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl index c1090d0a1..bcf36e229 100644 --- a/loc/lcl/PLK/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl +++ b/loc/lcl/PLK/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl @@ -3,6 +3,7 @@ + @@ -432,10 +433,13 @@ - + - + + + + @@ -577,6 +581,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/loc/lcl/PLK/Microsoft.VisualStudio.Threading.dll.lcl b/loc/lcl/PLK/Microsoft.VisualStudio.Threading.dll.lcl index 879234905..38944da90 100644 --- a/loc/lcl/PLK/Microsoft.VisualStudio.Threading.dll.lcl +++ b/loc/lcl/PLK/Microsoft.VisualStudio.Threading.dll.lcl @@ -3,6 +3,7 @@ + @@ -130,6 +131,15 @@ + + + + + + + + + @@ -244,10 +254,13 @@ - + - + + + + diff --git a/loc/lcl/PLK/SosThreadingToolsManaged.dll.lcl b/loc/lcl/PLK/SosThreadingToolsManaged.dll.lcl new file mode 100644 index 000000000..c76fa026f --- /dev/null +++ b/loc/lcl/PLK/SosThreadingToolsManaged.dll.lcl @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/loc/lcl/PTB/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl b/loc/lcl/PTB/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl index e14c91a49..0579e9bc1 100644 --- a/loc/lcl/PTB/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl +++ b/loc/lcl/PTB/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl @@ -3,6 +3,7 @@ + @@ -432,10 +433,13 @@ - + - + + + + @@ -577,6 +581,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/loc/lcl/PTB/Microsoft.VisualStudio.Threading.dll.lcl b/loc/lcl/PTB/Microsoft.VisualStudio.Threading.dll.lcl index 91e3d4e8c..5a4986ab8 100644 --- a/loc/lcl/PTB/Microsoft.VisualStudio.Threading.dll.lcl +++ b/loc/lcl/PTB/Microsoft.VisualStudio.Threading.dll.lcl @@ -3,6 +3,7 @@ + @@ -130,6 +131,15 @@ + + + + + + + + + @@ -244,10 +254,13 @@ - + - + + + + diff --git a/loc/lcl/PTB/SosThreadingToolsManaged.dll.lcl b/loc/lcl/PTB/SosThreadingToolsManaged.dll.lcl new file mode 100644 index 000000000..6d3ab472c --- /dev/null +++ b/loc/lcl/PTB/SosThreadingToolsManaged.dll.lcl @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/loc/lcl/RUS/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl b/loc/lcl/RUS/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl index ce0eb6d48..8cc2a6ac7 100644 --- a/loc/lcl/RUS/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl +++ b/loc/lcl/RUS/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl @@ -3,6 +3,7 @@ + @@ -432,10 +433,13 @@ - + - + + + + @@ -577,6 +581,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/loc/lcl/RUS/Microsoft.VisualStudio.Threading.dll.lcl b/loc/lcl/RUS/Microsoft.VisualStudio.Threading.dll.lcl index 9b966d5d3..123e425e5 100644 --- a/loc/lcl/RUS/Microsoft.VisualStudio.Threading.dll.lcl +++ b/loc/lcl/RUS/Microsoft.VisualStudio.Threading.dll.lcl @@ -3,6 +3,7 @@ + @@ -130,6 +131,15 @@ + + + + + + + + + @@ -244,10 +254,13 @@ - + - + + + + diff --git a/loc/lcl/RUS/SosThreadingToolsManaged.dll.lcl b/loc/lcl/RUS/SosThreadingToolsManaged.dll.lcl new file mode 100644 index 000000000..d4193faed --- /dev/null +++ b/loc/lcl/RUS/SosThreadingToolsManaged.dll.lcl @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/loc/lcl/TRK/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl b/loc/lcl/TRK/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl index 4fb573710..8393d262a 100644 --- a/loc/lcl/TRK/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl +++ b/loc/lcl/TRK/Microsoft.VisualStudio.Threading.Analyzers.dll.lcl @@ -3,6 +3,7 @@ + @@ -432,10 +433,13 @@ - + - + + + + @@ -577,6 +581,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/loc/lcl/TRK/Microsoft.VisualStudio.Threading.dll.lcl b/loc/lcl/TRK/Microsoft.VisualStudio.Threading.dll.lcl index 24280f33a..3d5116402 100644 --- a/loc/lcl/TRK/Microsoft.VisualStudio.Threading.dll.lcl +++ b/loc/lcl/TRK/Microsoft.VisualStudio.Threading.dll.lcl @@ -3,6 +3,7 @@ + @@ -130,6 +131,15 @@ + + + + + + + + + @@ -244,10 +254,13 @@ - + - + + + + diff --git a/loc/lcl/TRK/SosThreadingToolsManaged.dll.lcl b/loc/lcl/TRK/SosThreadingToolsManaged.dll.lcl new file mode 100644 index 000000000..cf6660d31 --- /dev/null +++ b/loc/lcl/TRK/SosThreadingToolsManaged.dll.lcl @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/nuget.config b/nuget.config index 40c3009ef..35d15c110 100644 --- a/nuget.config +++ b/nuget.config @@ -2,24 +2,13 @@ - - - - - + - - - Microsoft;xunit;manuel.roemer;sharwell;jamesnk;aarnott;MarcoRossignoli;Thecentury;clairernovotny;reg;mmanela;onovotny - - - - - - - - + + + + diff --git a/samples/.editorconfig b/samples/.editorconfig new file mode 100644 index 000000000..726d351c3 --- /dev/null +++ b/samples/.editorconfig @@ -0,0 +1,60 @@ +[*.cs] + +indent_style = space + +# SA1108: Block statements should not contain embedded comments +dotnet_diagnostic.SA1108.severity = none + +# SA1123: Do not place regions within elements +dotnet_diagnostic.SA1123.severity = none + +# SA1124: Do not use regions +dotnet_diagnostic.SA1124.severity = none + +# SA1200: Using directives should be placed correctly +dotnet_diagnostic.SA1200.severity = none + +# SA1201: Elements should appear in the correct order +dotnet_diagnostic.SA1201.severity = silent + +# SA1205: Partial elements should declare access +dotnet_diagnostic.SA1205.severity = none + +# SA1400: Access modifier should be declared +dotnet_diagnostic.SA1400.severity = none + +# SA1402: File may only contains a single type +dotnet_diagnostic.SA1402.severity = none + +# SA1403: File may only contain a single namespace +dotnet_diagnostic.SA1403.severity = none + +# SA1502: Element should not be on a single line +dotnet_diagnostic.SA1502.severity = none + +# SA1515: Single-line comment should be preceded by blank line +dotnet_diagnostic.SA1515.severity = none + +# SA1516: Elements should be separated by blank line +dotnet_diagnostic.SA1516.severity = none + +# SA1600: Elements should be documented +dotnet_diagnostic.SA1600.severity = silent + +# SA1601: Partial elements should be documented +dotnet_diagnostic.SA1601.severity = silent + +# SA1649: File name should match first type name +dotnet_diagnostic.SA1649.severity = none + +# IDE0051: Remove unused private members +dotnet_diagnostic.IDE0051.severity = none + +# CS1591: Missing XML comment for publicly visible type or member +dotnet_diagnostic.CS1591.severity = silent + +# CA1822: Mark members as static +dotnet_diagnostic.CA1822.severity = silent + +# CA1062: Validate arguments of public methods +dotnet_diagnostic.CA1062.severity = silent diff --git a/samples/DisableProcessing.cs b/samples/DisableProcessing.cs new file mode 100644 index 000000000..0deddb1d1 --- /dev/null +++ b/samples/DisableProcessing.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#pragma warning disable VSTHRD103 // Call async methods when in an async method + +using System.IO; +using Microsoft.VisualStudio.Threading; + +internal class DisableProcessing +{ + private readonly JoinableTaskFactory joinableTaskFactory = null!; + + private void Simple() + { + #region Simple + this.joinableTaskFactory.Run(async delegate + { + this.joinableTaskFactory.DisableProcessing(); + + // Synchronous I/O and lock contentions will NOT result in any reentrancy within this JoinableTask. + }); + #endregion + } + + private void Exhaustive() + { + #region Exhaustive + this.joinableTaskFactory.Run(async delegate + { + // Async I/O isn't expected to synchronously block, and thus would never allow unwanted reentrancy. + string content = await File.ReadAllTextAsync(@"somefile.txt"); + + // Here, synchronous I/O and lock contentions MAY allow certain reentrancy (e.g. COM RPC messages). + content = File.ReadAllText(@"somefile.txt"); + + using (this.joinableTaskFactory.DisableProcessing()) + { + // Within this block, synchronous I/O and lock contentions will NOT result in any reentrancy. + content = File.ReadAllText(@"somefile.txt"); + } + + // Just disable the synchronous wait message pump for the rest of this JoinableTask. + this.joinableTaskFactory.DisableProcessing(); + + // Sync I/O and lock contentions will NOT result in any reentrancy here. + content = File.ReadAllText(@"somefile.txt"); + }); + #endregion + } +} diff --git a/samples/Polyfill.cs b/samples/Polyfill.cs new file mode 100644 index 000000000..5853f02ea --- /dev/null +++ b/samples/Polyfill.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if NETFRAMEWORK + +using System; +using System.IO; +using System.Threading.Tasks; + +internal static class PolyfillExtensions +{ + extension(File) + { + internal static Task ReadAllTextAsync(string path) => throw new NotImplementedException(); + } +} + +#endif diff --git a/samples/SuppressRelevance.cs b/samples/SuppressRelevance.cs new file mode 100644 index 000000000..348eb2ce6 --- /dev/null +++ b/samples/SuppressRelevance.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Threading.Tasks; +using Microsoft.VisualStudio.Threading; + +public class SuppressRelevance +{ + private readonly ReentrantSemaphore semaphore = ReentrantSemaphore.Create(1, null, ReentrantSemaphore.ReentrancyMode.NotAllowed); + + #region SuppressRelevance + public async Task DoSomethingAsync() + { + await this.semaphore.ExecuteAsync(async delegate + { + // field access under the semaphore + // ... + await Task.Yield(); // represents some async work + + // Fire and forget code that uses the semaphore, but should *not* + // inherit our own possession of the semaphore. + using (this.semaphore.SuppressRelevance()) + { + this.DoSomethingLaterAsync().Forget(); // Don't await this, or a deadlock will occur. + } + }); + } + + private async Task DoSomethingLaterAsync() + { + // This semaphore use will not be seen as nested because of our caller's wrapping + // the call in SuppressRelevance. + // So instead of throwing, it will block till its caller releases the semaphore. + await this.semaphore.ExecuteAsync(async delegate + { + // Whatever + await Task.Yield(); // represents some async work + }); + } + #endregion +} diff --git a/samples/samples.csproj b/samples/samples.csproj new file mode 100644 index 000000000..6f1681d0a --- /dev/null +++ b/samples/samples.csproj @@ -0,0 +1,28 @@ + + + + net8.0;net472 + false + + + + + + false + Analyzer + + + false + Analyzer + + + false + Analyzer + + + + + + + + diff --git a/settings.VisualStudio.json b/settings.VisualStudio.json new file mode 100644 index 000000000..7abb4a060 --- /dev/null +++ b/settings.VisualStudio.json @@ -0,0 +1,3 @@ +{ + "textEditor.codeCleanup.profile": "profile1" +} diff --git a/src/Analyzers.props b/src/Analyzers.props new file mode 100644 index 000000000..bdb4fd3ee --- /dev/null +++ b/src/Analyzers.props @@ -0,0 +1,7 @@ + + + + + true + + diff --git a/src/AssemblyInfo.cs b/src/AssemblyInfo.cs index dcdd281db..9731a8304 100644 --- a/src/AssemblyInfo.cs +++ b/src/AssemblyInfo.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Runtime.InteropServices; diff --git a/src/AssemblyInfo.vb b/src/AssemblyInfo.vb new file mode 100644 index 000000000..75fe6ea4c --- /dev/null +++ b/src/AssemblyInfo.vb @@ -0,0 +1,6 @@ +' Copyright (c) Microsoft Corporation. All rights reserved. +' Licensed under the MIT license. See LICENSE file in the project root for full license information. + +Imports System.Runtime.InteropServices + + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index c73680b58..74a5dbf11 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,5 +1,14 @@ + - + + + README.md + + + + + + PackageIcon.png diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 566ab4fcf..654f5c6d3 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -1,7 +1,9 @@ + - + + - + diff --git a/src/LibraryNuspecProperties.props b/src/LibraryNuspecProperties.props new file mode 100644 index 000000000..51565ee53 --- /dev/null +++ b/src/LibraryNuspecProperties.props @@ -0,0 +1,12 @@ + + + netstandard2.0;net8.0;net472 + $(TargetFrameworks);net8.0-windows + + Async synchronization primitives, async collections, TPL and dataflow extensions. + The JoinableTaskFactory allows synchronously blocking the UI thread for async work. This + package is applicable to any .NET application (not just Visual Studio). + + Threading Async Lock Synchronization Threadsafe + + diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/AssemblyInfo.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/AssemblyInfo.cs index 18c0aa280..61f8ea2f6 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/AssemblyInfo.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/AssemblyInfo.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -10,6 +10,3 @@ [assembly: ComVisible(false)] [assembly: NeutralResourcesLanguage("en-US")] - -[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Threading.Analyzers.CodeFixes, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] -[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Threading.Analyzers.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpCommonInterest.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpCommonInterest.cs index fd460ed23..497469f95 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpCommonInterest.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpCommonInterest.cs @@ -1,291 +1,290 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +internal static class CSharpCommonInterest { - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Linq; - using System.Text; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CSharp; - using Microsoft.CodeAnalysis.CSharp.Syntax; - using Microsoft.CodeAnalysis.Diagnostics; - - internal static class CSharpCommonInterest + internal static readonly IImmutableSet MethodSyntaxKinds = ImmutableHashSet.Create( + SyntaxKind.ConstructorDeclaration, + SyntaxKind.MethodDeclaration, + SyntaxKind.OperatorDeclaration, + SyntaxKind.AnonymousMethodExpression, + SyntaxKind.SimpleLambdaExpression, + SyntaxKind.ParenthesizedLambdaExpression, + SyntaxKind.GetAccessorDeclaration, + SyntaxKind.SetAccessorDeclaration, + SyntaxKind.AddAccessorDeclaration, + SyntaxKind.RemoveAccessorDeclaration); + + /// + /// This is an explicit rule to ignore the code that was generated by Xaml2CS. + /// + /// + /// The generated code has the comments like this: + /// + /// ]]> + /// This rule is based on the fact the keyword "<auto-generated>" should be found in the comments. + /// + internal static bool ShouldIgnoreContext(SyntaxNodeAnalysisContext context) { - internal static readonly IImmutableSet MethodSyntaxKinds = ImmutableHashSet.Create( - SyntaxKind.ConstructorDeclaration, - SyntaxKind.MethodDeclaration, - SyntaxKind.OperatorDeclaration, - SyntaxKind.AnonymousMethodExpression, - SyntaxKind.SimpleLambdaExpression, - SyntaxKind.ParenthesizedLambdaExpression, - SyntaxKind.GetAccessorDeclaration, - SyntaxKind.SetAccessorDeclaration, - SyntaxKind.AddAccessorDeclaration, - SyntaxKind.RemoveAccessorDeclaration); - - /// - /// This is an explicit rule to ignore the code that was generated by Xaml2CS. - /// - /// - /// The generated code has the comments like this: - /// - /// ]]> - /// This rule is based on the fact the keyword "<auto-generated>" should be found in the comments. - /// - internal static bool ShouldIgnoreContext(SyntaxNodeAnalysisContext context) + NamespaceDeclarationSyntax? namespaceDeclaration = context.Node.FirstAncestorOrSelf(); + if (namespaceDeclaration is object) { - NamespaceDeclarationSyntax? namespaceDeclaration = context.Node.FirstAncestorOrSelf(); - if (namespaceDeclaration is object) + foreach (SyntaxTrivia trivia in namespaceDeclaration.NamespaceKeyword.GetAllTrivia()) { - foreach (SyntaxTrivia trivia in namespaceDeclaration.NamespaceKeyword.GetAllTrivia()) + const string autoGeneratedKeyword = @""; + if (trivia.FullSpan.Length > autoGeneratedKeyword.Length + && trivia.ToString().Contains(autoGeneratedKeyword)) { - const string autoGeneratedKeyword = @""; - if (trivia.FullSpan.Length > autoGeneratedKeyword.Length - && trivia.ToString().Contains(autoGeneratedKeyword)) - { - return true; - } + return true; } } - - return false; } - internal static void InspectMemberAccess( - SyntaxNodeAnalysisContext context, - MemberAccessExpressionSyntax? memberAccessSyntax, - DiagnosticDescriptor descriptor, - IEnumerable problematicMethods, - bool ignoreIfInsideAnonymousDelegate = false) + return false; + } + + internal static void InspectMemberAccess( + SyntaxNodeAnalysisContext context, + MemberAccessExpressionSyntax? memberAccessSyntax, + DiagnosticDescriptor descriptor, + IEnumerable problematicMethods, + bool ignoreIfInsideAnonymousDelegate = false) + { + if (descriptor is null) { - if (descriptor is null) - { - throw new ArgumentNullException(nameof(descriptor)); - } + throw new ArgumentNullException(nameof(descriptor)); + } - if (memberAccessSyntax is null) - { - return; - } + if (memberAccessSyntax is null) + { + return; + } - if (ShouldIgnoreContext(context)) - { - return; - } + if (ShouldIgnoreContext(context)) + { + return; + } - if (ignoreIfInsideAnonymousDelegate && context.Node.FirstAncestorOrSelf() is object) - { - // We do not analyze JTF.Run inside anonymous functions because - // they are so often used as callbacks where the signature is constrained. - return; - } + if (ignoreIfInsideAnonymousDelegate && context.Node.FirstAncestorOrSelf() is object) + { + // We do not analyze JTF.Run inside anonymous functions because + // they are so often used as callbacks where the signature is constrained. + return; + } - if (CSharpUtils.IsWithinNameOf(context.Node as ExpressionSyntax)) - { - // We do not consider arguments to nameof( ) because they do not represent invocations of code. - return; - } + if (CSharpUtils.IsWithinNameOf(context.Node as ExpressionSyntax)) + { + // We do not consider arguments to nameof( ) because they do not represent invocations of code. + return; + } - ITypeSymbol? typeReceiver = context.SemanticModel.GetTypeInfo(memberAccessSyntax.Expression).Type; - if (typeReceiver is object) + ITypeSymbol? typeReceiver = context.SemanticModel.GetTypeInfo(memberAccessSyntax.Expression).Type; + if (typeReceiver is object) + { + foreach (CommonInterest.SyncBlockingMethod item in problematicMethods) { - foreach (CommonInterest.SyncBlockingMethod item in problematicMethods) + if (memberAccessSyntax.Name.Identifier.Text == item.Method.Name && + typeReceiver.Name == item.Method.ContainingType.Name && + typeReceiver.BelongsToNamespace(item.Method.ContainingType.Namespace)) { - if (memberAccessSyntax.Name.Identifier.Text == item.Method.Name && - typeReceiver.Name == item.Method.ContainingType.Name && - typeReceiver.BelongsToNamespace(item.Method.ContainingType.Namespace)) + if (HasTaskCompleted(context, memberAccessSyntax)) { - if (HasTaskCompleted(context, memberAccessSyntax)) - { - return; - } - - Location? location = memberAccessSyntax.Name.GetLocation(); - context.ReportDiagnostic(Diagnostic.Create(descriptor, location)); + return; } + + Location? location = memberAccessSyntax.Name.GetLocation(); + context.ReportDiagnostic(Diagnostic.Create(descriptor, location)); } } } + } - private static SyntaxNode? GetEnclosingBlock(SyntaxNode node) + private static SyntaxNode? GetEnclosingBlock(SyntaxNode? node) + { + while (node is not null) { - while (node is not null) + if (node.IsKind(SyntaxKind.Block)) { - if (node.IsKind(SyntaxKind.Block)) - { - return node; - } - - node = node.Parent; + return node; } - return null; + node = node.Parent; } - private static bool IsVariablePassedToInvocation(InvocationExpressionSyntax invocationExpr, string variableName, bool byRef) + return null; + } + + private static bool IsVariablePassedToInvocation(InvocationExpressionSyntax invocationExpr, string variableName, bool byRef) + { + ArgumentListSyntax? argList = invocationExpr.ChildNodes().OfType().FirstOrDefault(); + if (argList is null) { - ArgumentListSyntax? argList = invocationExpr.ChildNodes().OfType().FirstOrDefault(); - if (argList is null) + return false; + } + + foreach (ArgumentSyntax arg in argList.ChildNodes().OfType()) + { + // `byRef` includes `out` parameters because they are the same as `ref` except don't require initialization first. + if (byRef && !arg.RefKindKeyword.IsKind(SyntaxKind.RefKeyword) && !arg.RefKindKeyword.IsKind(SyntaxKind.OutKeyword)) { - return false; + continue; } - foreach (ArgumentSyntax arg in argList.ChildNodes().OfType()) + IdentifierNameSyntax identiferName = arg.ChildNodes().OfType().FirstOrDefault(); + if (identiferName is null) { - // `byRef` includes `out` parameters because they are the same as `ref` except don't require initialization first. - if (byRef && !arg.RefKindKeyword.IsKind(SyntaxKind.RefKeyword) && !arg.RefKindKeyword.IsKind(SyntaxKind.OutKeyword)) - { - continue; - } - - IdentifierNameSyntax identiferName = arg.ChildNodes().OfType().FirstOrDefault(); - if (identiferName is null) - { - return false; - } + return false; + } - if (identiferName.Identifier.ValueText == variableName) - { - return true; - } + if (identiferName.Identifier.ValueText == variableName) + { + return true; } + } + return false; + } + + private static bool IsTaskCompletedWithWhenAll(SyntaxNodeAnalysisContext context, InvocationExpressionSyntax invocationExpr, string taskVariableName) + { + // We only care about awaited invocations, because an un-awaited Task.WhenAll will be an error. + if (invocationExpr.Parent is not AwaitExpressionSyntax) + { return false; } - private static bool IsTaskCompletedWithWhenAll(SyntaxNodeAnalysisContext context, InvocationExpressionSyntax invocationExpr, string taskVariableName) + IEnumerable? memberAccessList = invocationExpr.ChildNodes().OfType(); + if (memberAccessList.Count() != 1) { - // We only care about awaited invocations, because an un-awaited Task.WhenAll will be an error. - if (invocationExpr.Parent is not AwaitExpressionSyntax) - { - return false; - } + return false; + } - IEnumerable? memberAccessList = invocationExpr.ChildNodes().OfType(); - if (memberAccessList.Count() != 1) - { - return false; - } + MemberAccessExpressionSyntax? memberAccess = memberAccessList.First(); - MemberAccessExpressionSyntax? memberAccess = memberAccessList.First(); + // Does the invocation have the expected `Task.WhenAll` syntax? This is cheaper to verify before looking up its semantic type. + bool correctSyntax = memberAccess.Expression is IdentifierNameSyntax { Identifier.ValueText: Types.Task.TypeName } + && memberAccess.Name is IdentifierNameSyntax { Identifier.ValueText: Types.Task.WhenAll }; - // Does the invocation have the expected `Task.WhenAll` syntax? This is cheaper to verify before looking up its semantic type. - bool correctSyntax = memberAccess.Expression is IdentifierNameSyntax { Identifier.ValueText: Types.Task.TypeName } - && memberAccess.Name is IdentifierNameSyntax { Identifier.ValueText: Types.Task.WhenAll }; + if (!correctSyntax) + { + return false; + } - if (!correctSyntax) - { - return false; - } + // Is this `Task.WhenAll` invocation from the System.Threading.Tasks.Task type? + ITypeSymbol? classType = context.SemanticModel.GetTypeInfo(memberAccess.Expression).Type; + var correctType = classType?.Name == Types.Task.TypeName && classType.BelongsToNamespace(Types.Task.Namespace); + if (!correctType) + { + return false; + } - // Is this `Task.WhenAll` invocation from the System.Threading.Tasks.Task type? - ITypeSymbol? classType = context.SemanticModel.GetTypeInfo(memberAccess.Expression).Type; - var correctType = classType.Name == Types.Task.TypeName && classType.BelongsToNamespace(Types.Task.Namespace); - if (!correctType) - { - return false; - } + // Is the task variable passed as an argument to `Task.WhenAll`? + return IsVariablePassedToInvocation(invocationExpr, taskVariableName, byRef: false); + } - // Is the task variable passed as an argument to `Task.WhenAll`? - return IsVariablePassedToInvocation(invocationExpr, taskVariableName, byRef: false); + private static bool HasTaskCompleted(SyntaxNodeAnalysisContext context, MemberAccessExpressionSyntax memberAccessSyntax) + { + SyntaxNode? enclosingBlock = GetEnclosingBlock(memberAccessSyntax); + if (enclosingBlock is null) + { + return false; } - private static bool HasTaskCompleted(SyntaxNodeAnalysisContext context, MemberAccessExpressionSyntax memberAccessSyntax) + // Get the task variable name from the problematic member access expression so that we can later try + // and determine if it has been used in a `Task.WhenAll` invocation. + // Examples: + // task1.Result; + // task2.GetAwaiter().GetResult(); + string? taskVariableName = null; + ExpressionSyntax parentExpr = memberAccessSyntax.Expression; + while (parentExpr is not null) { - SyntaxNode? enclosingBlock = GetEnclosingBlock(memberAccessSyntax); - if (enclosingBlock is null) + if (parentExpr is IdentifierNameSyntax identifierExpr) { - return false; + taskVariableName = identifierExpr.Identifier.ValueText; + break; } - - // Get the task variable name from the problematic member access expression so that we can later try - // and determine if it has been used in a `Task.WhenAll` invocation. - // Examples: - // task1.Result; - // task2.GetAwaiter().GetResult(); - string? taskVariableName = null; - ExpressionSyntax parentExpr = memberAccessSyntax.Expression; - while (parentExpr is not null) + else if (parentExpr is MemberAccessExpressionSyntax memberAccessExpr) { - if (parentExpr is IdentifierNameSyntax identifierExpr) - { - taskVariableName = identifierExpr.Identifier.ValueText; - break; - } - else if (parentExpr is MemberAccessExpressionSyntax memberAccessExpr) - { - parentExpr = memberAccessExpr.Expression; - } - else if (parentExpr is InvocationExpressionSyntax invocExpr) - { - parentExpr = invocExpr.Expression; - } - else - { - break; - } + parentExpr = memberAccessExpr.Expression; } - - if (taskVariableName is null) + else if (parentExpr is InvocationExpressionSyntax invocExpr) { - return false; + parentExpr = invocExpr.Expression; } - - // Find all `Task.WhenAll` invocations that precede the problematic member access, which are also in the same enclosing block. - IEnumerable? taskWhenAllInvocationList = - from invoc in enclosingBlock.DescendantNodes().OfType() - where memberAccessSyntax.SpanStart > invoc.Span.End && - IsTaskCompletedWithWhenAll(context, invoc, taskVariableName) - select invoc; - - if (!taskWhenAllInvocationList.Any()) + else { - return false; + break; } + } - // If a `Task.WhenAll` invocation precedes the problematic member access, and the task variable has not been - // invalidated in between, then we consider the task to be completed. - // Example: - // await Task.WhenAll(task1, task2, task3); - // task1 = Task.Run(...); // Invalidates `task1` - // DoSomething(ref task2); // Invalidates `task2` - // task1.Result; // Warn - // task2.Result; // Warn - // task3.Result; // No warning, task3 has not been invalidated in between WhenAll and this problematic member access - foreach (InvocationExpressionSyntax? taskWhenAllInvocation in taskWhenAllInvocationList) - { - // Has the task variable been assigned to a new task? - IEnumerable? assignmentList = - from assign in enclosingBlock.DescendantNodes().OfType() - where assign.SpanStart > taskWhenAllInvocation.Span.End && - assign.SpanStart < memberAccessSyntax.SpanStart && - ((IdentifierNameSyntax)assign.Left).Identifier.ValueText == taskVariableName - select assign; - - if (assignmentList.Any()) - { - return false; - } + if (taskVariableName is null) + { + return false; + } + + // Find all `Task.WhenAll` invocations that precede the problematic member access, which are also in the same enclosing block. + IEnumerable? taskWhenAllInvocationList = + from invoc in enclosingBlock.DescendantNodes().OfType() + where memberAccessSyntax.SpanStart > invoc.Span.End && + IsTaskCompletedWithWhenAll(context, invoc, taskVariableName) + select invoc; - // Has the task variable been passed by ref to a method? - // If so, we must assume the worst case that the method has assigned it to a new task. - IEnumerable? invocationList = - from invoc in enclosingBlock.DescendantNodes().OfType() - where invoc.SpanStart > taskWhenAllInvocation.Span.End && - invoc.SpanStart < memberAccessSyntax.SpanStart && - IsVariablePassedToInvocation(invoc, taskVariableName, byRef: true) - select invoc; + if (!taskWhenAllInvocationList.Any()) + { + return false; + } - return !invocationList.Any(); + // If a `Task.WhenAll` invocation precedes the problematic member access, and the task variable has not been + // invalidated in between, then we consider the task to be completed. + // Example: + // await Task.WhenAll(task1, task2, task3); + // task1 = Task.Run(...); // Invalidates `task1` + // DoSomething(ref task2); // Invalidates `task2` + // task1.Result; // Warn + // task2.Result; // Warn + // task3.Result; // No warning, task3 has not been invalidated in between WhenAll and this problematic member access + foreach (InvocationExpressionSyntax? taskWhenAllInvocation in taskWhenAllInvocationList) + { + // Has the task variable been assigned to a new task? + IEnumerable? assignmentList = + from assign in enclosingBlock.DescendantNodes().OfType() + where assign.SpanStart > taskWhenAllInvocation.Span.End && + assign.SpanStart < memberAccessSyntax.SpanStart && + ((IdentifierNameSyntax)assign.Left).Identifier.ValueText == taskVariableName + select assign; + + if (assignmentList.Any()) + { + return false; } - return false; + // Has the task variable been passed by ref to a method? + // If so, we must assume the worst case that the method has assigned it to a new task. + IEnumerable? invocationList = + from invoc in enclosingBlock.DescendantNodes().OfType() + where invoc.SpanStart > taskWhenAllInvocation.Span.End && + invoc.SpanStart < memberAccessSyntax.SpanStart && + IsVariablePassedToInvocation(invoc, taskVariableName, byRef: true) + select invoc; + + return !invocationList.Any(); } + + return false; } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpUtils.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpUtils.cs index 67d2187e8..47292ea5b 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpUtils.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpUtils.cs @@ -1,281 +1,318 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +public sealed class CSharpUtils : LanguageUtils { - using System; - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - using System.Linq; - using System.Threading; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CSharp; - using Microsoft.CodeAnalysis.CSharp.Syntax; - using Microsoft.CodeAnalysis.Operations; - - internal sealed class CSharpUtils : LanguageUtils + public static readonly CSharpUtils Instance = new CSharpUtils(); + + private CSharpUtils() { - public static readonly CSharpUtils Instance = new CSharpUtils(); + } - private CSharpUtils() + public static ExpressionSyntax IsolateMethodName(InvocationExpressionSyntax invocation) + { + if (invocation is null) { + throw new ArgumentNullException(nameof(invocation)); } - internal static ExpressionSyntax IsolateMethodName(InvocationExpressionSyntax invocation) + var memberAccessExpression = invocation.Expression as MemberAccessExpressionSyntax; +#pragma warning disable CA1508 // Avoid dead conditional code + ExpressionSyntax invokedMethodName = memberAccessExpression?.Name ?? invocation.Expression as IdentifierNameSyntax ?? (invocation.Expression as MemberBindingExpressionSyntax)?.Name ?? invocation.Expression; +#pragma warning restore CA1508 // Avoid dead conditional code + return invokedMethodName; + } + + /// + /// Finds the local function, anonymous function, method, accessor, or ctor that most directly owns a given syntax node. + /// + /// The syntax node to begin the search from. + /// The containing function, and metadata for it. + public static ContainingFunctionData GetContainingFunction(CSharpSyntaxNode? syntaxNode) + { + while (syntaxNode is object) { - if (invocation is null) + if (syntaxNode is SimpleLambdaExpressionSyntax simpleLambda) { - throw new ArgumentNullException(nameof(invocation)); + return new ContainingFunctionData(simpleLambda, simpleLambda.AsyncKeyword != default(SyntaxToken), SyntaxFactory.ParameterList().AddParameters(simpleLambda.Parameter), simpleLambda.Body, simpleLambda.WithBody); } - var memberAccessExpression = invocation.Expression as MemberAccessExpressionSyntax; -#pragma warning disable CA1508 // Avoid dead conditional code - ExpressionSyntax invokedMethodName = memberAccessExpression?.Name ?? invocation.Expression as IdentifierNameSyntax ?? (invocation.Expression as MemberBindingExpressionSyntax)?.Name ?? invocation.Expression; -#pragma warning restore CA1508 // Avoid dead conditional code - return invokedMethodName; - } + if (syntaxNode is LocalFunctionStatementSyntax localFunc) + { + return new ContainingFunctionData(localFunc, localFunc.Modifiers.Any(SyntaxKind.AsyncKeyword), localFunc.ParameterList, (CSharpSyntaxNode?)localFunc.ExpressionBody ?? localFunc.Body, localFunc.WithBody); + } - /// - /// Finds the local function, anonymous function, method, accessor, or ctor that most directly owns a given syntax node. - /// - /// The syntax node to begin the search from. - /// The containing function, and metadata for it. - internal static ContainingFunctionData GetContainingFunction(CSharpSyntaxNode syntaxNode) - { - while (syntaxNode is object) + if (syntaxNode is AnonymousMethodExpressionSyntax anonymousMethod) { - if (syntaxNode is SimpleLambdaExpressionSyntax simpleLambda) - { - return new ContainingFunctionData(simpleLambda, simpleLambda.AsyncKeyword != default(SyntaxToken), SyntaxFactory.ParameterList().AddParameters(simpleLambda.Parameter), simpleLambda.Body, simpleLambda.WithBody); - } + return new ContainingFunctionData(anonymousMethod, anonymousMethod.AsyncKeyword != default(SyntaxToken), anonymousMethod.ParameterList, anonymousMethod.Body, anonymousMethod.WithBody); + } - if (syntaxNode is AnonymousMethodExpressionSyntax anonymousMethod) - { - return new ContainingFunctionData(anonymousMethod, anonymousMethod.AsyncKeyword != default(SyntaxToken), anonymousMethod.ParameterList, anonymousMethod.Body, anonymousMethod.WithBody); - } + if (syntaxNode is ParenthesizedLambdaExpressionSyntax lambda) + { + return new ContainingFunctionData(lambda, lambda.AsyncKeyword != default(SyntaxToken), lambda.ParameterList, lambda.Body, lambda.WithBody); + } - if (syntaxNode is ParenthesizedLambdaExpressionSyntax lambda) + if (syntaxNode is AccessorDeclarationSyntax accessor) + { + Func bodyReplacement = newBody => newBody switch { - return new ContainingFunctionData(lambda, lambda.AsyncKeyword != default(SyntaxToken), lambda.ParameterList, lambda.Body, lambda.WithBody); - } + BlockSyntax block => accessor.WithBody(block), + ArrowExpressionClauseSyntax expression => accessor.WithExpressionBody(expression), + _ => throw new NotSupportedException(), + }; + return new ContainingFunctionData(accessor, false, SyntaxFactory.ParameterList(), accessor.Body, bodyReplacement); + } - if (syntaxNode is AccessorDeclarationSyntax accessor) + if (syntaxNode is BaseMethodDeclarationSyntax method) + { + Func bodyReplacement = method switch { - Func bodyReplacement = newBody => newBody switch + MethodDeclarationSyntax m => (CSharpSyntaxNode newBody) => newBody switch { - BlockSyntax block => accessor.WithBody(block), - ArrowExpressionClauseSyntax expression => accessor.WithExpressionBody(expression), + ArrowExpressionClauseSyntax expr => m.WithExpressionBody(expr), + BlockSyntax block => m.WithBody(block), _ => throw new NotSupportedException(), - }; - return new ContainingFunctionData(accessor, false, SyntaxFactory.ParameterList(), accessor.Body, bodyReplacement); - } - - if (syntaxNode is BaseMethodDeclarationSyntax method) - { - Func bodyReplacement = method switch + }, + ConstructorDeclarationSyntax c => (CSharpSyntaxNode newBody) => newBody switch { - MethodDeclarationSyntax m => (CSharpSyntaxNode newBody) => newBody switch - { - ArrowExpressionClauseSyntax expr => m.WithExpressionBody(expr), - BlockSyntax block => m.WithBody(block), - _ => throw new NotSupportedException(), - }, - ConstructorDeclarationSyntax c => (CSharpSyntaxNode newBody) => newBody switch - { - ArrowExpressionClauseSyntax expr => c.WithExpressionBody(expr), - BlockSyntax block => c.WithBody(block), - _ => throw new NotSupportedException(), - }, - OperatorDeclarationSyntax o => (CSharpSyntaxNode newBody) => newBody switch - { - ArrowExpressionClauseSyntax expr => o.WithExpressionBody(expr), - BlockSyntax block => o.WithBody(block), - _ => throw new NotSupportedException(), - }, + ArrowExpressionClauseSyntax expr => c.WithExpressionBody(expr), + BlockSyntax block => c.WithBody(block), _ => throw new NotSupportedException(), - }; - return new ContainingFunctionData(method, method.Modifiers.Any(SyntaxKind.AsyncKeyword), method.ParameterList, method.Body, bodyReplacement); - } - - syntaxNode = (CSharpSyntaxNode)syntaxNode.Parent; + }, + OperatorDeclarationSyntax o => (CSharpSyntaxNode newBody) => newBody switch + { + ArrowExpressionClauseSyntax expr => o.WithExpressionBody(expr), + BlockSyntax block => o.WithBody(block), + _ => throw new NotSupportedException(), + }, + DestructorDeclarationSyntax d => (CSharpSyntaxNode newBody) => newBody switch + { + ArrowExpressionClauseSyntax expr => d.WithExpressionBody(expr), + BlockSyntax block => d.WithBody(block), + _ => throw new NotSupportedException(), + }, + _ => throw new NotSupportedException(), + }; + return new ContainingFunctionData(method, method.Modifiers.Any(SyntaxKind.AsyncKeyword), method.ParameterList, method.Body, bodyReplacement); } - return default(ContainingFunctionData); + syntaxNode = (CSharpSyntaxNode?)syntaxNode.Parent; } - internal static bool IsOnLeftHandOfAssignment(SyntaxNode syntaxNode) + return default(ContainingFunctionData); + } + + public static bool IsOnLeftHandOfAssignment(SyntaxNode syntaxNode) + { + SyntaxNode? parent = null; + while ((parent = syntaxNode.Parent) is object) { - SyntaxNode? parent = null; - while ((parent = syntaxNode.Parent) is object) + if (parent is AssignmentExpressionSyntax assignment) { - if (parent is AssignmentExpressionSyntax assignment) - { - return assignment.Left == syntaxNode; - } - - syntaxNode = parent; + return assignment.Left == syntaxNode; } - return false; + syntaxNode = parent; + } + + return false; + } + + public static IEnumerable FindAssignedValuesWithin(SyntaxNode container, SemanticModel semanticModel, ISymbol variable, CancellationToken cancellationToken) + { + if (semanticModel is null) + { + throw new ArgumentNullException(nameof(semanticModel)); } - internal static bool IsAssignedWithin(SyntaxNode container, SemanticModel semanticModel, ISymbol variable, CancellationToken cancellationToken) + if (variable is null) { - if (semanticModel is null) - { - throw new ArgumentNullException(nameof(semanticModel)); - } + throw new ArgumentNullException(nameof(variable)); + } - if (variable is null) - { - throw new ArgumentNullException(nameof(variable)); - } + if (container is null) + { + yield break; + } - if (container is null) + foreach (SyntaxNode? node in container.DescendantNodesAndSelf(n => !(n is AnonymousFunctionExpressionSyntax or LocalFunctionStatementSyntax))) + { + cancellationToken.ThrowIfCancellationRequested(); + if (node is AssignmentExpressionSyntax assignment) { - return false; + ISymbol? assignedSymbol = semanticModel.GetSymbolInfo(assignment.Left, cancellationToken).Symbol; + if (variable.Equals(assignedSymbol, SymbolEqualityComparer.Default)) + { + yield return assignment.Right; + } } - foreach (SyntaxNode? node in container.DescendantNodesAndSelf(n => !(n is AnonymousFunctionExpressionSyntax))) + if (node is LocalDeclarationStatementSyntax localDeclarationStatement) { - if (node is AssignmentExpressionSyntax assignment) + foreach (VariableDeclaratorSyntax localDeclVar in localDeclarationStatement.Declaration.Variables) { - ISymbol? assignedSymbol = semanticModel.GetSymbolInfo(assignment.Left, cancellationToken).Symbol; - if (variable.Equals(assignedSymbol)) + if (localDeclVar.Initializer is not null) { - return true; + ISymbol? assignedSymbol = semanticModel.GetDeclaredSymbol(localDeclVar, cancellationToken); + if (variable.Equals(assignedSymbol, SymbolEqualityComparer.Default)) + { + yield return localDeclVar.Initializer.Value; + } } } } - - return false; } + } - internal static MemberAccessExpressionSyntax MemberAccess(IReadOnlyList qualifiers, SimpleNameSyntax simpleName) + public static MemberAccessExpressionSyntax MemberAccess(IReadOnlyList qualifiers, SimpleNameSyntax simpleName) + { + if (qualifiers is null) { - if (qualifiers is null) - { - throw new ArgumentNullException(nameof(qualifiers)); - } - - if (simpleName is null) - { - throw new ArgumentNullException(nameof(simpleName)); - } - - if (qualifiers.Count == 0) - { - throw new ArgumentException("At least one qualifier required."); - } + throw new ArgumentNullException(nameof(qualifiers)); + } - ExpressionSyntax result = SyntaxFactory.IdentifierName(qualifiers[0]); - for (int i = 1; i < qualifiers.Count; i++) - { - IdentifierNameSyntax? rightSide = SyntaxFactory.IdentifierName(qualifiers[i]); - result = SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, result, rightSide); - } + if (simpleName is null) + { + throw new ArgumentNullException(nameof(simpleName)); + } - return SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, result, simpleName); + if (qualifiers.Count == 0) + { + throw new ArgumentException("At least one qualifier required."); } - /// - /// Determines whether an expression appears inside a C# "nameof" pseudo-method. - /// - internal static bool IsWithinNameOf([NotNullWhen(true)] SyntaxNode? syntaxNode) + ExpressionSyntax result = SyntaxFactory.IdentifierName(qualifiers[0]); + for (int i = 1; i < qualifiers.Count; i++) { - InvocationExpressionSyntax? invocation = syntaxNode?.FirstAncestorOrSelf(); - return invocation is object - && (invocation.Expression as IdentifierNameSyntax)?.Identifier.Text == "nameof" - && invocation.ArgumentList.Arguments.Count == 1; + IdentifierNameSyntax? rightSide = SyntaxFactory.IdentifierName(qualifiers[i]); + result = SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, result, rightSide); } - internal override Location? GetLocationOfBaseTypeName(INamedTypeSymbol symbol, INamedTypeSymbol baseType, Compilation compilation, CancellationToken cancellationToken) + return SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, result, simpleName); + } + + /// + /// Determines whether an expression appears inside a C# "nameof" pseudo-method. + /// + public static bool IsWithinNameOf([NotNullWhen(true)] SyntaxNode? syntaxNode) + { + InvocationExpressionSyntax? invocation = syntaxNode?.FirstAncestorOrSelf(); + return invocation is object + && (invocation.Expression as IdentifierNameSyntax)?.Identifier.Text == "nameof" + && invocation.ArgumentList.Arguments.Count == 1; + } + + public override Location? GetLocationOfBaseTypeName(INamedTypeSymbol symbol, INamedTypeSymbol baseType, Compilation compilation, CancellationToken cancellationToken) + { + foreach (SyntaxReference? syntaxReference in symbol.DeclaringSyntaxReferences) { - foreach (SyntaxReference? syntaxReference in symbol.DeclaringSyntaxReferences) + SyntaxNode? syntaxNode = syntaxReference.GetSyntax(cancellationToken); + if (syntaxNode is TypeDeclarationSyntax { BaseList: { } } typeDeclarationSyntax) { - SyntaxNode? syntaxNode = syntaxReference.GetSyntax(cancellationToken); - if (syntaxNode is TypeDeclarationSyntax { BaseList: { } } typeDeclarationSyntax) + if (compilation.GetSemanticModel(typeDeclarationSyntax.SyntaxTree) is { } semanticModel) { - if (compilation.GetSemanticModel(typeDeclarationSyntax.SyntaxTree) is { } semanticModel) + foreach (BaseTypeSyntax? baseTypeSyntax in typeDeclarationSyntax.BaseList.Types) { - foreach (BaseTypeSyntax? baseTypeSyntax in typeDeclarationSyntax.BaseList.Types) + SymbolInfo baseTypeSymbolInfo = semanticModel.GetSymbolInfo(baseTypeSyntax.Type, cancellationToken); + if (SymbolEqualityComparer.Default.Equals(baseTypeSymbolInfo.Symbol, baseType)) { - SymbolInfo baseTypeSymbolInfo = semanticModel.GetSymbolInfo(baseTypeSyntax.Type, cancellationToken); - if (Equals(baseTypeSymbolInfo.Symbol, baseType)) - { - return baseTypeSyntax.GetLocation(); - } + return baseTypeSyntax.GetLocation(); } } } } - - return symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(cancellationToken)?.GetLocation(); } - internal override SyntaxNode IsolateMethodName(IInvocationOperation invocation) - { - if (invocation.Syntax is InvocationExpressionSyntax invocationExpression) - { - return IsolateMethodName(invocationExpression); - } + return symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(cancellationToken)?.GetLocation(); + } - return invocation.Syntax; + public override SyntaxNode IsolateMethodName(IInvocationOperation invocation) + { + if (invocation.Syntax is InvocationExpressionSyntax invocationExpression) + { + return IsolateMethodName(invocationExpression); } - internal override SyntaxNode IsolateMethodName(IObjectCreationOperation objectCreation) - { - if (objectCreation.Syntax is ObjectCreationExpressionSyntax { Type: { } type }) - { - return type; - } + return invocation.Syntax; + } - return objectCreation.Syntax; + public override SyntaxNode IsolateMethodName(IObjectCreationOperation objectCreation) + { + if (objectCreation.Syntax is ObjectCreationExpressionSyntax { Type: { } type }) + { + return type; } - internal override bool MethodReturnsNullableReferenceType(IMethodSymbol methodSymbol) + return objectCreation.Syntax; + } + + public override bool MethodReturnsNullableReferenceType(IMethodSymbol methodSymbol) + { + SyntaxReference? syntaxReference = methodSymbol.DeclaringSyntaxReferences.FirstOrDefault(); + if (syntaxReference is null) { - SyntaxReference? syntaxReference = methodSymbol.DeclaringSyntaxReferences.FirstOrDefault(); - if (syntaxReference is null) - { - return false; - } + return false; + } - SyntaxNode syntaxNode = syntaxReference.GetSyntax(); - TypeSyntax? returnType = null; + SyntaxNode syntaxNode = syntaxReference.GetSyntax(); + TypeSyntax? returnType = null; - if (syntaxNode is MethodDeclarationSyntax methodDeclSyntax) - { - returnType = methodDeclSyntax.ReturnType; - } - else if (syntaxNode is LocalFunctionStatementSyntax localFunc) - { - returnType = localFunc.ReturnType; - } - - return returnType is not null && returnType.IsKind(SyntaxKind.NullableType); + if (syntaxNode is MethodDeclarationSyntax methodDeclSyntax) + { + returnType = methodDeclSyntax.ReturnType; + } + else if (syntaxNode is LocalFunctionStatementSyntax localFunc) + { + returnType = localFunc.ReturnType; } - internal readonly struct ContainingFunctionData + return returnType is not null && returnType.IsKind(SyntaxKind.NullableType); + } + + public override bool IsAsyncMethod(SyntaxNode syntaxNode) + { + SyntaxTokenList? modifiers = syntaxNode switch { - internal ContainingFunctionData(CSharpSyntaxNode function, bool isAsync, ParameterListSyntax parameterList, CSharpSyntaxNode blockOrExpression, Func bodyReplacement) - { - this.Function = function; - this.IsAsync = isAsync; - this.ParameterList = parameterList; - this.BlockOrExpression = blockOrExpression; - this.BodyReplacement = bodyReplacement; - } + MethodDeclarationSyntax methodDeclaration => methodDeclaration.Modifiers, + SimpleLambdaExpressionSyntax lambda => lambda.Modifiers, + AnonymousMethodExpressionSyntax anonMethod => anonMethod.Modifiers, + ParenthesizedLambdaExpressionSyntax lambda => lambda.Modifiers, + _ => null, + }; + return modifiers?.Any(SyntaxKind.AsyncKeyword) is true; + } - internal CSharpSyntaxNode Function { get; } + public readonly struct ContainingFunctionData + { + public ContainingFunctionData(CSharpSyntaxNode function, bool isAsync, ParameterListSyntax? parameterList, CSharpSyntaxNode? blockOrExpression, Func bodyReplacement) + { + this.Function = function; + this.IsAsync = isAsync; + this.ParameterList = parameterList; + this.BlockOrExpression = blockOrExpression; + this.BodyReplacement = bodyReplacement; + } - internal bool IsAsync { get; } + public CSharpSyntaxNode Function { get; } - internal ParameterListSyntax ParameterList { get; } + public bool IsAsync { get; } - internal CSharpSyntaxNode BlockOrExpression { get; } + public ParameterListSyntax? ParameterList { get; } - internal Func BodyReplacement { get; } - } + public CSharpSyntaxNode? BlockOrExpression { get; } + + public Func BodyReplacement { get; } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD001UseSwitchToMainThreadAsyncAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD001UseSwitchToMainThreadAsyncAnalyzer.cs index 01848cb7d..7ad45e4ab 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD001UseSwitchToMainThreadAsyncAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD001UseSwitchToMainThreadAsyncAnalyzer.cs @@ -1,14 +1,13 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers; - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public sealed class CSharpVSTHRD001UseSwitchToMainThreadAsyncAnalyzer : AbstractVSTHRD001UseSwitchToMainThreadAsyncAnalyzer - { - private protected override LanguageUtils LanguageUtils => CSharpUtils.Instance; - } +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class CSharpVSTHRD001UseSwitchToMainThreadAsyncAnalyzer : AbstractVSTHRD001UseSwitchToMainThreadAsyncAnalyzer +{ + protected override LanguageUtils LanguageUtils => CSharpUtils.Instance; } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD004AwaitSwitchToMainThreadAsyncAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD004AwaitSwitchToMainThreadAsyncAnalyzer.cs index 5580c3131..3dbd93ed1 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD004AwaitSwitchToMainThreadAsyncAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD004AwaitSwitchToMainThreadAsyncAnalyzer.cs @@ -1,14 +1,13 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers; - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public sealed class CSharpVSTHRD004AwaitSwitchToMainThreadAsyncAnalyzer : AbstractVSTHRD004AwaitSwitchToMainThreadAsyncAnalyzer - { - private protected override LanguageUtils LanguageUtils => CSharpUtils.Instance; - } +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class CSharpVSTHRD004AwaitSwitchToMainThreadAsyncAnalyzer : AbstractVSTHRD004AwaitSwitchToMainThreadAsyncAnalyzer +{ + protected override LanguageUtils LanguageUtils => CSharpUtils.Instance; } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD011UseAsyncLazyAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD011UseAsyncLazyAnalyzer.cs index 33f4c8e33..38a4361ce 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD011UseAsyncLazyAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD011UseAsyncLazyAnalyzer.cs @@ -1,14 +1,13 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers; - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public sealed class CSharpVSTHRD011UseAsyncLazyAnalyzer : AbstractVSTHRD011UseAsyncLazyAnalyzer - { - private protected override LanguageUtils LanguageUtils => CSharpUtils.Instance; - } +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class CSharpVSTHRD011UseAsyncLazyAnalyzer : AbstractVSTHRD011UseAsyncLazyAnalyzer +{ + protected override LanguageUtils LanguageUtils => CSharpUtils.Instance; } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD012SpecifyJtfWhereAllowed.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD012SpecifyJtfWhereAllowed.cs index 2143f3959..9dd4d6666 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD012SpecifyJtfWhereAllowed.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD012SpecifyJtfWhereAllowed.cs @@ -1,14 +1,13 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers; - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public sealed class CSharpVSTHRD012SpecifyJtfWhereAllowed : AbstractVSTHRD012SpecifyJtfWhereAllowed - { - private protected override LanguageUtils LanguageUtils => CSharpUtils.Instance; - } +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class CSharpVSTHRD012SpecifyJtfWhereAllowed : AbstractVSTHRD012SpecifyJtfWhereAllowed +{ + protected override LanguageUtils LanguageUtils => CSharpUtils.Instance; } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzer.cs index 94ee09e17..cde8a1b12 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzer.cs @@ -1,14 +1,13 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers; - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public sealed class CSharpVSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzer : AbstractVSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzer - { - private protected override LanguageUtils LanguageUtils => CSharpUtils.Instance; - } +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class CSharpVSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzer : AbstractVSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzer +{ + protected override LanguageUtils LanguageUtils => CSharpUtils.Instance; } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD108AssertThreadRequirementUnconditionally.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD108AssertThreadRequirementUnconditionally.cs index 3a40bbe93..26c43d0ef 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD108AssertThreadRequirementUnconditionally.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD108AssertThreadRequirementUnconditionally.cs @@ -1,14 +1,13 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers; - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public sealed class CSharpVSTHRD108AssertThreadRequirementUnconditionally : AbstractVSTHRD108AssertThreadRequirementUnconditionally - { - private protected override LanguageUtils LanguageUtils => CSharpUtils.Instance; - } +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class CSharpVSTHRD108AssertThreadRequirementUnconditionally : AbstractVSTHRD108AssertThreadRequirementUnconditionally +{ + protected override LanguageUtils LanguageUtils => CSharpUtils.Instance; } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD109AvoidAssertInAsyncMethodsAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD109AvoidAssertInAsyncMethodsAnalyzer.cs index 6e620baac..d52c02608 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD109AvoidAssertInAsyncMethodsAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD109AvoidAssertInAsyncMethodsAnalyzer.cs @@ -1,14 +1,13 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers; - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public sealed class CSharpVSTHRD109AvoidAssertInAsyncMethodsAnalyzer : AbstractVSTHRD109AvoidAssertInAsyncMethodsAnalyzer - { - private protected override LanguageUtils LanguageUtils => CSharpUtils.Instance; - } +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class CSharpVSTHRD109AvoidAssertInAsyncMethodsAnalyzer : AbstractVSTHRD109AvoidAssertInAsyncMethodsAnalyzer +{ + protected override LanguageUtils LanguageUtils => CSharpUtils.Instance; } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD110ObserveResultOfAsyncCallsAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD110ObserveResultOfAsyncCallsAnalyzer.cs new file mode 100644 index 000000000..7c71225a7 --- /dev/null +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD110ObserveResultOfAsyncCallsAnalyzer.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class CSharpVSTHRD110ObserveResultOfAsyncCallsAnalyzer : AbstractVSTHRD110ObserveResultOfAsyncCallsAnalyzer +{ + protected override LanguageUtils LanguageUtils => CSharpUtils.Instance; +} diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD112ImplementSystemIAsyncDisposableAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD112ImplementSystemIAsyncDisposableAnalyzer.cs index dbaf87ba6..021d111c3 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD112ImplementSystemIAsyncDisposableAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD112ImplementSystemIAsyncDisposableAnalyzer.cs @@ -1,14 +1,14 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + namespace Microsoft.VisualStudio.Threading.Analyzers { - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; - [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class CSharpVSTHRD112ImplementSystemIAsyncDisposableAnalyzer : AbstractVSTHRD112ImplementSystemIAsyncDisposableAnalyzer { - private protected override LanguageUtils LanguageUtils => CSharpUtils.Instance; + protected override LanguageUtils LanguageUtils => CSharpUtils.Instance; } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD114AvoidReturningNullTaskAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD114AvoidReturningNullTaskAnalyzer.cs index b0c5edd73..4e74ad8ac 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD114AvoidReturningNullTaskAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpVSTHRD114AvoidReturningNullTaskAnalyzer.cs @@ -1,14 +1,13 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers; - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public sealed class CSharpVSTHRD114AvoidReturningNullTaskAnalyzer : AbstractVSTHRD114AvoidReturningNullTaskAnalyzer - { - private protected override LanguageUtils LanguageUtils => CSharpUtils.Instance; - } +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class CSharpVSTHRD114AvoidReturningNullTaskAnalyzer : AbstractVSTHRD114AvoidReturningNullTaskAnalyzer +{ + protected override LanguageUtils LanguageUtils => CSharpUtils.Instance; } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/Microsoft.VisualStudio.Threading.Analyzers.CSharp.csproj b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/Microsoft.VisualStudio.Threading.Analyzers.CSharp.csproj index 0005abbe0..d834a1b3d 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/Microsoft.VisualStudio.Threading.Analyzers.CSharp.csproj +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/Microsoft.VisualStudio.Threading.Analyzers.CSharp.csproj @@ -1,13 +1,16 @@  + - netstandard1.3 + netstandard2.0 Microsoft.VisualStudio.Threading.Analyzers + true + false false - - + + diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD002UseJtfRunAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD002UseJtfRunAnalyzer.cs index cdf1fa7bd..5a0727902 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD002UseJtfRunAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD002UseJtfRunAnalyzer.cs @@ -1,159 +1,158 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CSharp; - using Microsoft.CodeAnalysis.CSharp.Syntax; - using Microsoft.CodeAnalysis.Diagnostics; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; - /// - /// Report warnings when detect the code that is waiting on tasks or awaiters synchronously. - /// - /// - /// [Background] or will often deadlock if - /// they are called on main thread, because now it is synchronously blocking the main thread for the - /// completion of a task that may need the main thread to complete. Even if they are called on a threadpool - /// thread, it is occupying a threadpool thread to do nothing but block, which is not good either. - /// - /// i.e. - /// - /// var task = Task.Run(DoSomethingOnBackground); - /// task.Wait(); /* This analyzer will report warning on this synchronous wait. */ - /// - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public class VSTHRD002UseJtfRunAnalyzer : DiagnosticAnalyzer - { - public const string Id = "VSTHRD002"; +namespace Microsoft.VisualStudio.Threading.Analyzers; - internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD002_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD002_MessageFormat), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); +/// +/// Report warnings when detect the code that is waiting on tasks or awaiters synchronously. +/// +/// +/// [Background] or will often deadlock if +/// they are called on main thread, because now it is synchronously blocking the main thread for the +/// completion of a task that may need the main thread to complete. Even if they are called on a threadpool +/// thread, it is occupying a threadpool thread to do nothing but block, which is not good either. +/// +/// i.e. +/// +/// var task = Task.Run(DoSomethingOnBackground); +/// task.Wait(); /* This analyzer will report warning on this synchronous wait. */ +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class VSTHRD002UseJtfRunAnalyzer : DiagnosticAnalyzer +{ + public const string Id = "VSTHRD002"; - /// - public override ImmutableArray SupportedDiagnostics + public static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD002_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD002_MessageFormat), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + /// + public override ImmutableArray SupportedDiagnostics + { + get { - get - { - return ImmutableArray.Create(Descriptor); - } + return ImmutableArray.Create(Descriptor); } + } - /// - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + /// + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); - context.RegisterCompilationStartAction(compilationContext => + context.RegisterCompilationStartAction(compilationContext => + { + INamedTypeSymbol? taskSymbol = compilationContext.Compilation.GetTypeByMetadataName(Types.Task.FullName); + if (taskSymbol is object) { - INamedTypeSymbol? taskSymbol = compilationContext.Compilation.GetTypeByMetadataName(Types.Task.FullName); - if (taskSymbol is object) + compilationContext.RegisterCodeBlockStartAction(codeBlockContext => { - compilationContext.RegisterCodeBlockStartAction(codeBlockContext => + // We want to scan properties and methods that do not return Task or Task. + var methodSymbol = codeBlockContext.OwningSymbol as IMethodSymbol; + var propertySymbol = codeBlockContext.OwningSymbol as IPropertySymbol; + if (propertySymbol is object || (methodSymbol is object && !methodSymbol.HasAsyncCompatibleReturnType())) { - // We want to scan properties and methods that do not return Task or Task. - var methodSymbol = codeBlockContext.OwningSymbol as IMethodSymbol; - var propertySymbol = codeBlockContext.OwningSymbol as IPropertySymbol; - if (propertySymbol is object || (methodSymbol is object && !methodSymbol.HasAsyncCompatibleReturnType())) - { - codeBlockContext.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(c => AnalyzeInvocation(c, taskSymbol)), SyntaxKind.InvocationExpression); - codeBlockContext.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(c => AnalyzeMemberAccess(c, taskSymbol)), SyntaxKind.SimpleMemberAccessExpression); - } - }); - } - }); - } - - private static ParameterSyntax? GetFirstParameter(AnonymousFunctionExpressionSyntax? anonymousFunctionSyntax) - { - switch (anonymousFunctionSyntax) - { - case SimpleLambdaExpressionSyntax lambda: - return lambda.Parameter; - case ParenthesizedLambdaExpressionSyntax lambda: - return lambda.ParameterList.Parameters.FirstOrDefault(); - case AnonymousMethodExpressionSyntax anonymousMethod: - return anonymousMethod.ParameterList?.Parameters.FirstOrDefault(); + codeBlockContext.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(c => AnalyzeInvocation(c, taskSymbol)), SyntaxKind.InvocationExpression); + codeBlockContext.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(c => AnalyzeMemberAccess(c, taskSymbol)), SyntaxKind.SimpleMemberAccessExpression); + } + }); } + }); + } - return null; + private static ParameterSyntax? GetFirstParameter(AnonymousFunctionExpressionSyntax? anonymousFunctionSyntax) + { + switch (anonymousFunctionSyntax) + { + case SimpleLambdaExpressionSyntax lambda: + return lambda.Parameter; + case ParenthesizedLambdaExpressionSyntax lambda: + return lambda.ParameterList.Parameters.FirstOrDefault(); + case AnonymousMethodExpressionSyntax anonymousMethod: + return anonymousMethod.ParameterList?.Parameters.FirstOrDefault(); } - private static void InspectMemberAccess( - SyntaxNodeAnalysisContext context, - MemberAccessExpressionSyntax? memberAccessSyntax, - IEnumerable problematicMethods, - INamedTypeSymbol taskSymbol) + return null; + } + + private static void InspectMemberAccess( + SyntaxNodeAnalysisContext context, + MemberAccessExpressionSyntax? memberAccessSyntax, + IEnumerable problematicMethods, + INamedTypeSymbol taskSymbol) + { + if (memberAccessSyntax is null) { - if (memberAccessSyntax is null) - { - return; - } + return; + } - // Are we in the context of an anonymous function that is passed directly in as an argument to another method? - AnonymousFunctionExpressionSyntax? anonymousFunctionSyntax = context.Node.FirstAncestorOrSelf(); - var anonFuncAsArgument = anonymousFunctionSyntax?.Parent as ArgumentSyntax; - var invocationPassingExpression = anonFuncAsArgument?.Parent?.Parent as InvocationExpressionSyntax; - var invokedMemberAccess = invocationPassingExpression?.Expression as MemberAccessExpressionSyntax; - if (invokedMemberAccess?.Name is object) + // Are we in the context of an anonymous function that is passed directly in as an argument to another method? + AnonymousFunctionExpressionSyntax? anonymousFunctionSyntax = context.Node.FirstAncestorOrSelf(); + var anonFuncAsArgument = anonymousFunctionSyntax?.Parent as ArgumentSyntax; + var invocationPassingExpression = anonFuncAsArgument?.Parent?.Parent as InvocationExpressionSyntax; + var invokedMemberAccess = invocationPassingExpression?.Expression as MemberAccessExpressionSyntax; + if (invokedMemberAccess?.Name is object) + { + // Does the anonymous function appear as the first argument to Task.ContinueWith? + var invokedMemberSymbol = context.SemanticModel.GetSymbolInfo(invokedMemberAccess.Name, context.CancellationToken).Symbol as IMethodSymbol; + if (invokedMemberSymbol?.Name == nameof(Task.ContinueWith) && + Utils.IsEqualToOrDerivedFrom(invokedMemberSymbol?.ContainingType, taskSymbol) && + invocationPassingExpression?.ArgumentList?.Arguments.FirstOrDefault() == anonFuncAsArgument) { - // Does the anonymous function appear as the first argument to Task.ContinueWith? - var invokedMemberSymbol = context.SemanticModel.GetSymbolInfo(invokedMemberAccess.Name, context.CancellationToken).Symbol as IMethodSymbol; - if (invokedMemberSymbol?.Name == nameof(Task.ContinueWith) && - Utils.IsEqualToOrDerivedFrom(invokedMemberSymbol?.ContainingType, taskSymbol) && - invocationPassingExpression?.ArgumentList?.Arguments.FirstOrDefault() == anonFuncAsArgument) + // Does the member access being analyzed belong to the Task that just completed? + ParameterSyntax? firstParameter = GetFirstParameter(anonymousFunctionSyntax); + if (firstParameter is object) { - // Does the member access being analyzed belong to the Task that just completed? - ParameterSyntax? firstParameter = GetFirstParameter(anonymousFunctionSyntax); - if (firstParameter is object) + // Are we accessing a member of the completed task? + ISymbol? invokedObjectSymbol = context.SemanticModel.GetSymbolInfo(memberAccessSyntax.Expression, context.CancellationToken).Symbol; + IParameterSymbol? completedTask = context.SemanticModel.GetDeclaredSymbol(firstParameter); + if (EqualityComparer.Default.Equals(invokedObjectSymbol, completedTask)) { - // Are we accessing a member of the completed task? - ISymbol invokedObjectSymbol = context.SemanticModel.GetSymbolInfo(memberAccessSyntax.Expression, context.CancellationToken).Symbol; - IParameterSymbol completedTask = context.SemanticModel.GetDeclaredSymbol(firstParameter); - if (EqualityComparer.Default.Equals(invokedObjectSymbol, completedTask)) - { - // Skip analysis since Task.Result (et. al) of a completed Task is fair game. - return; - } + // Skip analysis since Task.Result (et. al) of a completed Task is fair game. + return; } } } - - CSharpCommonInterest.InspectMemberAccess(context, memberAccessSyntax, Descriptor, problematicMethods); } - private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context, INamedTypeSymbol taskSymbol) - { - var invocationExpressionSyntax = (InvocationExpressionSyntax)context.Node; - InspectMemberAccess( - context, - invocationExpressionSyntax.Expression as MemberAccessExpressionSyntax, - CommonInterest.ProblematicSyncBlockingMethods, - taskSymbol); - } + CSharpCommonInterest.InspectMemberAccess(context, memberAccessSyntax, Descriptor, problematicMethods); + } - private static void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context, INamedTypeSymbol taskSymbol) - { - var memberAccessSyntax = (MemberAccessExpressionSyntax)context.Node; - InspectMemberAccess( - context, - memberAccessSyntax, - CommonInterest.SyncBlockingProperties, - taskSymbol); - } + private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context, INamedTypeSymbol taskSymbol) + { + var invocationExpressionSyntax = (InvocationExpressionSyntax)context.Node; + InspectMemberAccess( + context, + invocationExpressionSyntax.Expression as MemberAccessExpressionSyntax, + CommonInterest.ProblematicSyncBlockingMethods, + taskSymbol); + } + + private static void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context, INamedTypeSymbol taskSymbol) + { + var memberAccessSyntax = (MemberAccessExpressionSyntax)context.Node; + InspectMemberAccess( + context, + memberAccessSyntax, + CommonInterest.SyncBlockingProperties, + taskSymbol); } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD003UseJtfRunAsyncAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD003UseJtfRunAsyncAnalyzer.cs index c33579333..19864c651 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD003UseJtfRunAsyncAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD003UseJtfRunAsyncAnalyzer.cs @@ -1,308 +1,361 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +/// +/// Detects await Task inside JoinableTaskFactory.Run or RunAsync. +/// +/// +/// [Background] Calling await on a Task inside a JoinableTaskFactory.Run, when the task is initialized outside the delegate can cause potential deadlocks. +/// This problem can be avoided by ensuring the task is initialized within the delegate or by using JoinableTask instead of Task.", +/// +/// i.e. +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class VSTHRD003UseJtfRunAsyncAnalyzer : DiagnosticAnalyzer { - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Diagnostics; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CSharp; - using Microsoft.CodeAnalysis.CSharp.Syntax; - using Microsoft.CodeAnalysis.Diagnostics; - - /// - /// Detects await Task inside JoinableTaskFactory.Run or RunAsync. - /// - /// - /// [Background] Calling await on a Task inside a JoinableTaskFactory.Run, when the task is initialized outside the delegate can cause potential deadlocks. - /// This problem can be avoided by ensuring the task is initialized within the delegate or by using JoinableTask instead of Task.", - /// - /// i.e. - /// - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public class VSTHRD003UseJtfRunAsyncAnalyzer : DiagnosticAnalyzer + public const string Id = "VSTHRD003"; + + internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD003_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD003_MessageFormat), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + /// + public override ImmutableArray SupportedDiagnostics { - public const string Id = "VSTHRD003"; - - internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD003_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD003_MessageFormat), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); - - /// - public override ImmutableArray SupportedDiagnostics + get { - get - { - return ImmutableArray.Create(Descriptor); - } + return ImmutableArray.Create(Descriptor); } + } - /// - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); - - context.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(this.AnalyzeAwaitExpression), SyntaxKind.AwaitExpression); - context.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(this.AnalyzeReturnStatement), SyntaxKind.ReturnStatement); - context.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(this.AnalyzeArrowExpressionClause), SyntaxKind.ArrowExpressionClause); - context.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(this.AnalyzeLambdaExpression), SyntaxKind.SimpleLambdaExpression); - context.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(this.AnalyzeLambdaExpression), SyntaxKind.ParenthesizedLambdaExpression); - } + /// + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + + context.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(this.AnalyzeAwaitExpression), SyntaxKind.AwaitExpression); + context.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(this.AnalyzeReturnStatement), SyntaxKind.ReturnStatement); + context.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(this.AnalyzeArrowExpressionClause), SyntaxKind.ArrowExpressionClause); + context.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(this.AnalyzeLambdaExpression), SyntaxKind.SimpleLambdaExpression); + context.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(this.AnalyzeLambdaExpression), SyntaxKind.ParenthesizedLambdaExpression); + } - private void AnalyzeArrowExpressionClause(SyntaxNodeAnalysisContext context) + private static bool IsSymbolAlwaysOkToAwait(ISymbol? symbol) + { + if (symbol is IFieldSymbol field) { - var arrowExpressionClause = (ArrowExpressionClauseSyntax)context.Node; - if (arrowExpressionClause.Parent is MethodDeclarationSyntax) + // Allow the TplExtensions.CompletedTask and related fields. + if (field.ContainingType.Name == Types.TplExtensions.TypeName && field.BelongsToNamespace(Types.TplExtensions.Namespace) && + (field.Name == Types.TplExtensions.CompletedTask || field.Name == Types.TplExtensions.CanceledTask || field.Name == Types.TplExtensions.TrueTask || field.Name == Types.TplExtensions.FalseTask)) { - Diagnostic? diagnostic = this.AnalyzeAwaitedOrReturnedExpression(arrowExpressionClause.Expression, context, context.CancellationToken); - if (diagnostic is object) - { - context.ReportDiagnostic(diagnostic); - } + return true; } } - - private void AnalyzeLambdaExpression(SyntaxNodeAnalysisContext context) + else if (symbol is IPropertySymbol property) { - var lambdaExpression = (LambdaExpressionSyntax)context.Node; - if (lambdaExpression.Body is ExpressionSyntax expression) + // Explicitly allow Task.CompletedTask + if (property.ContainingType.Name == Types.Task.TypeName && property.BelongsToNamespace(Types.Task.Namespace) && + property.Name == Types.Task.CompletedTask) { - Diagnostic? diagnostic = this.AnalyzeAwaitedOrReturnedExpression(expression, context, context.CancellationToken); - if (diagnostic is object) - { - context.ReportDiagnostic(diagnostic); - } + return true; } } - private void AnalyzeReturnStatement(SyntaxNodeAnalysisContext context) + return false; + } + + private void AnalyzeArrowExpressionClause(SyntaxNodeAnalysisContext context) + { + var arrowExpressionClause = (ArrowExpressionClauseSyntax)context.Node; + if (arrowExpressionClause.Parent is MethodDeclarationSyntax) { - var returnStatement = (ReturnStatementSyntax)context.Node; - Diagnostic? diagnostic = this.AnalyzeAwaitedOrReturnedExpression(returnStatement.Expression, context, context.CancellationToken); + Diagnostic? diagnostic = this.AnalyzeAwaitedOrReturnedExpression(arrowExpressionClause.Expression, context, context.CancellationToken); if (diagnostic is object) { context.ReportDiagnostic(diagnostic); } } + } - private void AnalyzeAwaitExpression(SyntaxNodeAnalysisContext context) + private void AnalyzeLambdaExpression(SyntaxNodeAnalysisContext context) + { + var lambdaExpression = (LambdaExpressionSyntax)context.Node; + if (lambdaExpression.Body is ExpressionSyntax expression) { - AwaitExpressionSyntax awaitExpressionSyntax = (AwaitExpressionSyntax)context.Node; - Diagnostic? diagnostic = this.AnalyzeAwaitedOrReturnedExpression(awaitExpressionSyntax.Expression, context, context.CancellationToken); + Diagnostic? diagnostic = this.AnalyzeAwaitedOrReturnedExpression(expression, context, context.CancellationToken); if (diagnostic is object) { context.ReportDiagnostic(diagnostic); } } + } + + private void AnalyzeReturnStatement(SyntaxNodeAnalysisContext context) + { + var returnStatement = (ReturnStatementSyntax)context.Node; + Diagnostic? diagnostic = this.AnalyzeAwaitedOrReturnedExpression(returnStatement.Expression, context, context.CancellationToken); + if (diagnostic is object) + { + context.ReportDiagnostic(diagnostic); + } + } + + private void AnalyzeAwaitExpression(SyntaxNodeAnalysisContext context) + { + AwaitExpressionSyntax awaitExpressionSyntax = (AwaitExpressionSyntax)context.Node; + Diagnostic? diagnostic = this.AnalyzeAwaitedOrReturnedExpression(awaitExpressionSyntax.Expression, context, context.CancellationToken); + if (diagnostic is object) + { + context.ReportDiagnostic(diagnostic); + } + } - private Diagnostic? AnalyzeAwaitedOrReturnedExpression(ExpressionSyntax expressionSyntax, SyntaxNodeAnalysisContext context, CancellationToken cancellationToken) + private Diagnostic? AnalyzeAwaitedOrReturnedExpression(ExpressionSyntax? expressionSyntax, SyntaxNodeAnalysisContext context, CancellationToken cancellationToken) + { + if (expressionSyntax is null) { - if (expressionSyntax is null) + return null; + } + + // Get the semantic model for the SyntaxTree for the given ExpressionSyntax, since it *may* not be in the same syntax tree + // as the original context.Node. + if (!context.TryGetNewOrExistingSemanticModel(expressionSyntax.SyntaxTree, out SemanticModel? semanticModel)) + { + return null; + } + + ExpressionSyntax focusedExpression = expressionSyntax; + SymbolInfo symbolToConsider = semanticModel.GetSymbolInfo(focusedExpression, cancellationToken); + if (CommonInterest.TaskConfigureAwait.Any(configureAwait => configureAwait.IsMatch(symbolToConsider.Symbol))) + { + // If the invocation is wrapped inside parentheses then drill down to get the invocation. + while (focusedExpression is ParenthesizedExpressionSyntax parenthesizedExprSyntax) { - return null; + focusedExpression = parenthesizedExprSyntax.Expression; } - // Get the semantic model for the SyntaxTree for the given ExpressionSyntax, since it *may* not be in the same syntax tree - // as the original context.Node. - if (!context.TryGetNewOrExistingSemanticModel(expressionSyntax.SyntaxTree, out SemanticModel? semanticModel)) + Debug.Assert(focusedExpression is InvocationExpressionSyntax, "focusedExpression should be an invocation"); + + if (((InvocationExpressionSyntax)focusedExpression).Expression is MemberAccessExpressionSyntax memberAccessExpression) { - return null; + focusedExpression = memberAccessExpression.Expression; + symbolToConsider = semanticModel.GetSymbolInfo(memberAccessExpression.Expression, cancellationToken); } + } - SymbolInfo symbolToConsider = semanticModel.GetSymbolInfo(expressionSyntax, cancellationToken); - if (CommonInterest.TaskConfigureAwait.Any(configureAwait => configureAwait.IsMatch(symbolToConsider.Symbol))) - { - // If the invocation is wrapped inside parentheses then drill down to get the invocation. - while (expressionSyntax is ParenthesizedExpressionSyntax parenthesizedExprSyntax) + ITypeSymbol symbolType; + bool dataflowAnalysisCompatibleVariable = false; + CSharpUtils.ContainingFunctionData? containingFunc = null; + switch (symbolToConsider.Symbol) + { + case ILocalSymbol localSymbol: + symbolType = localSymbol.Type; + dataflowAnalysisCompatibleVariable = true; + break; + case IPropertySymbol propertySymbol when !IsSymbolAlwaysOkToAwait(propertySymbol): + symbolType = propertySymbol.Type; + + if (focusedExpression is MemberAccessExpressionSyntax memberAccessExpression) { - expressionSyntax = parenthesizedExprSyntax.Expression; + // Do not report a warning if the task is a member of an object that was returned from an invocation made in this method. + if (memberAccessExpression.Expression is InvocationExpressionSyntax) + { + return null; + } + + // Do not report a warning if the task is a member of an object that was created in this method. + if (memberAccessExpression.Expression is IdentifierNameSyntax identifier) + { + ISymbol? symbol = semanticModel.GetSymbolInfo(identifier, cancellationToken).Symbol; + switch (symbol) + { + case ILocalSymbol local: + // Search for assignments to the local and see if it was to a new object or the result of an invocation. + containingFunc ??= CSharpUtils.GetContainingFunction(focusedExpression); + if (containingFunc.Value.BlockOrExpression is not null && + CSharpUtils.FindAssignedValuesWithin(containingFunc.Value.BlockOrExpression, semanticModel, local, cancellationToken).Any( + v => v is ObjectCreationExpressionSyntax or ImplicitObjectCreationExpressionSyntax or InvocationExpressionSyntax or AwaitExpressionSyntax { Expression: InvocationExpressionSyntax })) + { + return null; + } + + break; + case IParameterSymbol parameter: + // We allow returning members of a parameter in a lambda, to support `.Select(x => x.Completion)` syntax. + if (parameter.ContainingSymbol is IMethodSymbol method && method.MethodKind == MethodKind.AnonymousFunction) + { + return null; + } + + break; + } + } } - Debug.Assert(expressionSyntax is InvocationExpressionSyntax, "expressionSyntax should be an invocation"); + break; + case IParameterSymbol parameterSymbol: + symbolType = parameterSymbol.Type; + dataflowAnalysisCompatibleVariable = true; + break; + case IFieldSymbol fieldSymbol: + symbolType = fieldSymbol.Type; - if (((InvocationExpressionSyntax)expressionSyntax).Expression is MemberAccessExpressionSyntax memberAccessExpression) + // If the field is readonly and initialized with Task.FromResult, it's OK. + if (fieldSymbol.IsReadOnly) { - symbolToConsider = semanticModel.GetSymbolInfo(memberAccessExpression.Expression, cancellationToken); - } - } + // If we can find the source code for the field, we can check whether it has a field initializer + // that stores the result of a Task.FromResult invocation. + if (!fieldSymbol.DeclaringSyntaxReferences.Any()) + { + // No syntax for it at all. So outside the compilation. It *probably* is a precompleted cached task, so don't create a diagnostic. + return null; + } - ITypeSymbol symbolType; - bool dataflowAnalysisCompatibleVariable = false; - switch (symbolToConsider.Symbol) - { - case ILocalSymbol localSymbol: - symbolType = localSymbol.Type; - dataflowAnalysisCompatibleVariable = true; - break; - case IParameterSymbol parameterSymbol: - symbolType = parameterSymbol.Type; - dataflowAnalysisCompatibleVariable = true; - break; - case IFieldSymbol fieldSymbol: - symbolType = fieldSymbol.Type; - - // If the field is readonly and initialized with Task.FromResult, it's OK. - if (fieldSymbol.IsReadOnly) + foreach (SyntaxReference? syntaxReference in fieldSymbol.DeclaringSyntaxReferences) { - // If we can find the source code for the field, we can check whether it has a field initializer - // that stores the result of a Task.FromResult invocation. - if (!fieldSymbol.DeclaringSyntaxReferences.Any()) + if (syntaxReference.GetSyntax(cancellationToken) is VariableDeclaratorSyntax declarationSyntax) { - // No syntax for it at all. So outside the compilation. It *probably* is a precompleted cached task, so don't create a diagnostic. - return null; - } + if (declarationSyntax.Initializer?.Value is InvocationExpressionSyntax invocationSyntax && + invocationSyntax.Expression is object) + { + if (!context.Compilation.ContainsSyntaxTree(invocationSyntax.SyntaxTree)) + { + // We can't look up the definition of the field. It *probably* is a precompleted cached task, so don't create a diagnostic. + return null; + } - foreach (SyntaxReference? syntaxReference in fieldSymbol.DeclaringSyntaxReferences) - { - if (syntaxReference.GetSyntax(cancellationToken) is VariableDeclaratorSyntax declarationSyntax) + // Allow Task.From*() methods. + if (!context.TryGetNewOrExistingSemanticModel(invocationSyntax.SyntaxTree, out SemanticModel? declarationSemanticModel)) + { + return null; + } + + if (declarationSemanticModel.GetSymbolInfo(invocationSyntax.Expression, cancellationToken).Symbol is IMethodSymbol invokedMethod && + invokedMethod.ContainingType.Name == nameof(Task) && + invokedMethod.ContainingType.BelongsToNamespace(Types.Task.Namespace) && + (invokedMethod.Name == nameof(Task.FromResult) || invokedMethod.Name == nameof(Task.FromCanceled) || invokedMethod.Name == nameof(Task.FromException))) + { + return null; + } + } + else if (declarationSyntax.Initializer?.Value is MemberAccessExpressionSyntax memberAccessSyntax && memberAccessSyntax.Expression is object) { - if (declarationSyntax.Initializer?.Value is InvocationExpressionSyntax invocationSyntax && - invocationSyntax.Expression is object) + if (!context.TryGetNewOrExistingSemanticModel(memberAccessSyntax.SyntaxTree, out SemanticModel? declarationSemanticModel)) { - if (!context.Compilation.ContainsSyntaxTree(invocationSyntax.SyntaxTree)) - { - // We can't look up the definition of the field. It *probably* is a precompleted cached task, so don't create a diagnostic. - return null; - } - - // Whitelist Task.From*() methods. - if (!context.TryGetNewOrExistingSemanticModel(invocationSyntax.SyntaxTree, out SemanticModel? declarationSemanticModel)) - { - return null; - } - - if (declarationSemanticModel.GetSymbolInfo(invocationSyntax.Expression, cancellationToken).Symbol is IMethodSymbol invokedMethod && - invokedMethod.ContainingType.Name == nameof(Task) && - invokedMethod.ContainingType.BelongsToNamespace(Types.Task.Namespace) && - (invokedMethod.Name == nameof(Task.FromResult) || invokedMethod.Name == nameof(Task.FromCanceled) || invokedMethod.Name == nameof(Task.FromException))) - { - return null; - } + return null; } - else if (declarationSyntax.Initializer?.Value is MemberAccessExpressionSyntax memberAccessSyntax && memberAccessSyntax.Expression is object) + + ISymbol? definition = declarationSemanticModel.GetSymbolInfo(memberAccessSyntax, cancellationToken).Symbol; + if (IsSymbolAlwaysOkToAwait(definition)) { - if (!context.TryGetNewOrExistingSemanticModel(memberAccessSyntax.SyntaxTree, out SemanticModel? declarationSemanticModel)) - { - return null; - } - - ISymbol? definition = declarationSemanticModel.GetSymbolInfo(memberAccessSyntax, cancellationToken).Symbol; - if (definition is IFieldSymbol field) - { - // Whitelist the TplExtensions.CompletedTask and related fields. - if (field.ContainingType.Name == Types.TplExtensions.TypeName && field.BelongsToNamespace(Types.TplExtensions.Namespace) && - (field.Name == Types.TplExtensions.CompletedTask || field.Name == Types.TplExtensions.CanceledTask || field.Name == Types.TplExtensions.TrueTask || field.Name == Types.TplExtensions.FalseTask)) - { - return null; - } - } - else if (definition is IPropertySymbol property) - { - // Explicitly allow Task.CompletedTask - if (property.ContainingType.Name == Types.Task.TypeName && property.BelongsToNamespace(Types.Task.Namespace) && - property.Name == Types.Task.CompletedTask) - { - return null; - } - } + return null; } } } } + } - break; - case IMethodSymbol methodSymbol: - if (Utils.IsTask(methodSymbol.ReturnType) && expressionSyntax is InvocationExpressionSyntax invocationExpressionSyntax) - { - // Consider all arguments - IEnumerable? expressionsToConsider = invocationExpressionSyntax.ArgumentList.Arguments.Select(a => a.Expression); + break; + case IMethodSymbol methodSymbol: + if (Utils.IsTask(methodSymbol.ReturnType) && focusedExpression is InvocationExpressionSyntax invocationExpressionSyntax) + { + // Consider all arguments + IEnumerable? expressionsToConsider = invocationExpressionSyntax.ArgumentList.Arguments.Select(a => a.Expression); - // Consider the implicit first argument when this method is invoked as an extension method. - if (methodSymbol.IsExtensionMethod && invocationExpressionSyntax.Expression is MemberAccessExpressionSyntax invokedMember) + // Consider the implicit first argument when this method is invoked as an extension method. + if (methodSymbol.IsExtensionMethod && invocationExpressionSyntax.Expression is MemberAccessExpressionSyntax invokedMember) + { + if (!methodSymbol.ContainingType.Equals(semanticModel.GetSymbolInfo(invokedMember.Expression, cancellationToken).Symbol, SymbolEqualityComparer.Default)) { - if (!methodSymbol.ContainingType.Equals(semanticModel.GetSymbolInfo(invokedMember.Expression, cancellationToken).Symbol)) - { - expressionsToConsider = new ExpressionSyntax[] { invokedMember.Expression }.Concat(expressionsToConsider); - } + expressionsToConsider = new ExpressionSyntax[] { invokedMember.Expression }.Concat(expressionsToConsider); } - - return expressionsToConsider.Select(e => this.AnalyzeAwaitedOrReturnedExpression(e, context, cancellationToken)).FirstOrDefault(r => r is object); } - return null; - default: - return null; - } + return expressionsToConsider.Select(e => this.AnalyzeAwaitedOrReturnedExpression(e, context, cancellationToken)).FirstOrDefault(r => r is object); + } - if (symbolType?.Name != nameof(Task) || !symbolType.BelongsToNamespace(Namespaces.SystemThreadingTasks)) - { return null; - } + default: + return null; + } - // Report warning if the task was not initialized within the current delegate or lambda expression - CSharpUtils.ContainingFunctionData containingFunc = CSharpUtils.GetContainingFunction(expressionSyntax); - if (containingFunc.BlockOrExpression is BlockSyntax delegateBlock) - { - if (dataflowAnalysisCompatibleVariable) - { - // Run data flow analysis to understand where the task was defined - DataFlowAnalysis dataFlowAnalysis; + if (symbolType?.Name != nameof(Task) || !symbolType.BelongsToNamespace(Namespaces.SystemThreadingTasks)) + { + return null; + } - // When possible (await is direct child of the block and not a field), execute data flow analysis by passing first and last statement to capture only what happens before the await - // Check if the await is direct child of the code block (first parent is ExpressionStantement, second parent is the block itself) - if (delegateBlock.Equals(expressionSyntax.Parent.Parent?.Parent)) - { - dataFlowAnalysis = semanticModel.AnalyzeDataFlow(delegateBlock.ChildNodes().First(), expressionSyntax.Parent.Parent); - } - else - { - // Otherwise analyze the data flow for the entire block. One caveat: it doesn't distinguish if the initalization happens after the await. - dataFlowAnalysis = semanticModel.AnalyzeDataFlow(delegateBlock); - } + // Report warning if the task was not initialized within the current delegate or lambda expression + containingFunc ??= CSharpUtils.GetContainingFunction(focusedExpression); + if (containingFunc.Value.BlockOrExpression is BlockSyntax delegateBlock) + { + if (dataflowAnalysisCompatibleVariable) + { + // Run data flow analysis to understand where the task was defined + DataFlowAnalysis? dataFlowAnalysis; - if (!dataFlowAnalysis.WrittenInside.Contains(symbolToConsider.Symbol)) - { - return Diagnostic.Create(Descriptor, expressionSyntax.GetLocation()); - } + // When possible (await is direct child of the block and not a field), execute data flow analysis by passing first and last statement to capture only what happens before the await + // Check if the await is direct child of the code block (first parent is ExpressionStantement, second parent is the block itself) + if (delegateBlock.Equals(focusedExpression.Parent?.Parent?.Parent)) + { + dataFlowAnalysis = semanticModel.AnalyzeDataFlow(delegateBlock.ChildNodes().First(), focusedExpression.Parent.Parent); } else { - // Do the best we can searching for assignment statements. - if (!CSharpUtils.IsAssignedWithin(containingFunc.BlockOrExpression, semanticModel, symbolToConsider.Symbol, cancellationToken)) - { - return Diagnostic.Create(Descriptor, expressionSyntax.GetLocation()); - } + // Otherwise analyze the data flow for the entire block. One caveat: it doesn't distinguish if the initalization happens after the await. + dataFlowAnalysis = semanticModel.AnalyzeDataFlow(delegateBlock); + } + + if (dataFlowAnalysis?.WrittenInside.Contains(symbolToConsider.Symbol) is false) + { + return Diagnostic.Create(Descriptor, focusedExpression.GetLocation()); } } else { - // It's not a block, it's just a lambda expression, so the variable must be external. - return Diagnostic.Create(Descriptor, expressionSyntax.GetLocation()); + // Do the best we can searching for assignment statements. + if (!CSharpUtils.FindAssignedValuesWithin(containingFunc.Value.BlockOrExpression, semanticModel, symbolToConsider.Symbol, cancellationToken).Any()) + { + return Diagnostic.Create(Descriptor, focusedExpression.GetLocation()); + } } - - return null; } + else + { + // It's not a block, it's just a lambda expression, so the variable must be external. + return Diagnostic.Create(Descriptor, focusedExpression.GetLocation()); + } + + return null; } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD010MainThreadUsageAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD010MainThreadUsageAnalyzer.cs index 191d0e2b1..26b9a5af0 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD010MainThreadUsageAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD010MainThreadUsageAnalyzer.cs @@ -1,495 +1,496 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +/// +/// Flag usage of objects that must only be invoked while on the main thread (e.g. STA COM objects) +/// without having first verified that the current thread is main thread either by throwing if on +/// the wrong thread or asynchronously switching to the main thread. +/// +/// +/// [Background] Most of Visual Studio services especially the legacy services which are implemented in native code +/// are living in STA. Invoking such STA services from background thread would do COM marshaling. The calling background +/// thread will block and wait until the invocation is processed by the STA service on the main thread. It is not only about +/// inefficiency. Such COM marshaling might lead to dead lock if the method occupying the main thread is also waiting for +/// that calling background task and the main thread does not allow COM marshaling to reenter the main thread. To avoid potential +/// dead lock and the expensive COM marshaling, this analyzer would ask the caller of Visual Studio services to verify the +/// current thread is main thread, or switch to main thread prior invocation explicitly. +/// +/// i.e. +/// +/// IVsSolution sln = GetIVsSolution(); +/// sln.SetProperty(); /* This analyzer will report warning on this invocation. */ +/// +/// +/// i.e. +/// +/// ThreadHelper.ThrowIfNotOnUIThread(); +/// IVsSolution sln = GetIVsSolution(); +/// sln.SetProperty(); /* Good */ +/// +/// +/// i.e. +/// +/// await joinableTaskFactory.SwitchToMainThreadAsync(); +/// IVsSolution sln = GetIVsSolution(); +/// sln.SetProperty(); /* Good */ +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class VSTHRD010MainThreadUsageAnalyzer : DiagnosticAnalyzer { - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Linq; - using System.Threading; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CSharp; - using Microsoft.CodeAnalysis.CSharp.Syntax; - using Microsoft.CodeAnalysis.Diagnostics; - using Microsoft.CodeAnalysis.Operations; - using Microsoft.CodeAnalysis.Text; + public const string Id = "VSTHRD010"; /// - /// Flag usage of objects that must only be invoked while on the main thread (e.g. STA COM objects) - /// without having first verified that the current thread is main thread either by throwing if on - /// the wrong thread or asynchronously switching to the main thread. + /// The descriptor to use for diagnostics reported in synchronous methods. /// - /// - /// [Background] Most of Visual Studio services especially the legacy services which are implemented in native code - /// are living in STA. Invoking such STA services from background thread would do COM marshaling. The calling background - /// thread will block and wait until the invocation is processed by the STA service on the main thread. It is not only about - /// inefficiency. Such COM marshaling might lead to dead lock if the method occupying the main thread is also waiting for - /// that calling background task and the main thread does not allow COM marshaling to reenter the main thread. To avoid potential - /// dead lock and the expensive COM marshaling, this analyzer would ask the caller of Visual Studio services to verify the - /// current thread is main thread, or switch to main thread prior invocation explicitly. - /// - /// i.e. - /// - /// IVsSolution sln = GetIVsSolution(); - /// sln.SetProperty(); /* This analyzer will report warning on this invocation. */ - /// - /// - /// i.e. - /// - /// ThreadHelper.ThrowIfNotOnUIThread(); - /// IVsSolution sln = GetIVsSolution(); - /// sln.SetProperty(); /* Good */ - /// - /// - /// i.e. - /// - /// await joinableTaskFactory.SwitchToMainThreadAsync(); - /// IVsSolution sln = GetIVsSolution(); - /// sln.SetProperty(); /* Good */ - /// - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public class VSTHRD010MainThreadUsageAnalyzer : DiagnosticAnalyzer - { - public const string Id = "VSTHRD010"; + public static readonly DiagnosticDescriptor DescriptorSync = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD010_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD010_MessageFormat_Sync), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + customTags: WellKnownDiagnosticTags.CompilationEnd); + + /// + /// The descriptor to use for diagnostics reported in async methods. + /// + public static readonly DiagnosticDescriptor DescriptorAsync = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD010_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD010_MessageFormat_Async), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + customTags: WellKnownDiagnosticTags.CompilationEnd); + + /// + /// A reusable value to return from . + /// + private static readonly ImmutableArray ReusableSupportedDescriptors = ImmutableArray.Create( + DescriptorSync, + DescriptorAsync); + + private readonly LanguageUtils languageUtils = CSharpUtils.Instance; + private enum ThreadingContext + { /// - /// The descriptor to use for diagnostics reported in synchronous methods. + /// The context is not known, either because it was never asserted or switched to, + /// or because a branch in the method exists which changed the context conditionally. /// - internal static readonly DiagnosticDescriptor DescriptorSync = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD010_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD010_MessageFormat_Sync), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); + Unknown, /// - /// The descriptor to use for diagnostics reported in async methods. + /// The context is definitely on the main thread. /// - internal static readonly DiagnosticDescriptor DescriptorAsync = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD010_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD010_MessageFormat_Async), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); + MainThread, /// - /// A reusable value to return from . + /// The context is definitely on a non-UI thread. /// - private static readonly ImmutableArray ReusableSupportedDescriptors = ImmutableArray.Create( - DescriptorSync, - DescriptorAsync); + NotMainThread, + } - private readonly LanguageUtils languageUtils = CSharpUtils.Instance; + /// + public override ImmutableArray SupportedDiagnostics => ReusableSupportedDescriptors; + + /// + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); - private enum ThreadingContext + context.RegisterCompilationStartAction(compilationStartContext => { - /// - /// The context is not known, either because it was never asserted or switched to, - /// or because a branch in the method exists which changed the context conditionally. - /// - Unknown, - - /// - /// The context is definitely on the main thread. - /// - MainThread, - - /// - /// The context is definitely on a non-UI thread. - /// - NotMainThread, - } + var mainThreadAssertingMethods = CommonInterest.ReadMethods(compilationStartContext.Options, CommonInterest.FileNamePatternForMethodsThatAssertMainThread, compilationStartContext.CancellationToken).ToImmutableArray(); + var mainThreadSwitchingMethods = CommonInterest.ReadMethods(compilationStartContext.Options, CommonInterest.FileNamePatternForMethodsThatSwitchToMainThread, compilationStartContext.CancellationToken).ToImmutableArray(); + var membersRequiringMainThread = CommonInterest.ReadTypesAndMembers(compilationStartContext.Options, CommonInterest.FileNamePatternForMembersRequiringMainThread, compilationStartContext.CancellationToken).ToImmutableArray(); + ImmutableDictionary? diagnosticProperties = ImmutableDictionary.Empty + .Add(CommonInterest.FileNamePatternForMethodsThatAssertMainThread.ToString(), string.Join("\n", mainThreadAssertingMethods)) + .Add(CommonInterest.FileNamePatternForMethodsThatSwitchToMainThread.ToString(), string.Join("\n", mainThreadSwitchingMethods)); + + var methodsDeclaringUIThreadRequirement = new HashSet(SymbolEqualityComparer.Default); + var methodsAssertingUIThreadRequirement = new HashSet(SymbolEqualityComparer.Default); + var callerToCalleeMap = new Dictionary>(SymbolEqualityComparer.Default); + + compilationStartContext.RegisterCodeBlockStartAction(codeBlockStartContext => + { + var methodAnalyzer = new MethodAnalyzer( + mainThreadAssertingMethods: mainThreadAssertingMethods, + mainThreadSwitchingMethods: mainThreadSwitchingMethods, + membersRequiringMainThread: membersRequiringMainThread, + methodsDeclaringUIThreadRequirement: methodsDeclaringUIThreadRequirement, + methodsAssertingUIThreadRequirement: methodsAssertingUIThreadRequirement, + diagnosticProperties: diagnosticProperties); + codeBlockStartContext.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzeInvocation), SyntaxKind.InvocationExpression); + codeBlockStartContext.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzeMemberAccess), SyntaxKind.SimpleMemberAccessExpression); + codeBlockStartContext.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzeCast), SyntaxKind.CastExpression); + codeBlockStartContext.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzeAs), SyntaxKind.AsExpression); + codeBlockStartContext.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzeAs), SyntaxKind.IsExpression); + codeBlockStartContext.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzeIsPattern), SyntaxKind.IsPatternExpression); + }); - /// - public override ImmutableArray SupportedDiagnostics => ReusableSupportedDescriptors; + compilationStartContext.RegisterOperationAction(Utils.DebuggableWrapper(c => this.AddToCallerCalleeMap(c, callerToCalleeMap)), OperationKind.Invocation); + compilationStartContext.RegisterOperationAction(Utils.DebuggableWrapper(c => this.AddToCallerCalleeMap(c, callerToCalleeMap)), OperationKind.PropertyReference); - /// - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + // Strictly speaking, this will miss access to the underlying field, but there's no method to put in the map in that case + compilationStartContext.RegisterOperationAction(Utils.DebuggableWrapper(c => this.AddToCallerCalleeMap(c, callerToCalleeMap)), OperationKind.EventAssignment); - context.RegisterCompilationStartAction(compilationStartContext => + compilationStartContext.RegisterCompilationEndAction(compilationEndContext => { - var mainThreadAssertingMethods = CommonInterest.ReadMethods(compilationStartContext.Options, CommonInterest.FileNamePatternForMethodsThatAssertMainThread, compilationStartContext.CancellationToken).ToImmutableArray(); - var mainThreadSwitchingMethods = CommonInterest.ReadMethods(compilationStartContext.Options, CommonInterest.FileNamePatternForMethodsThatSwitchToMainThread, compilationStartContext.CancellationToken).ToImmutableArray(); - var membersRequiringMainThread = CommonInterest.ReadTypesAndMembers(compilationStartContext.Options, CommonInterest.FileNamePatternForMembersRequiringMainThread, compilationStartContext.CancellationToken).ToImmutableArray(); - ImmutableDictionary? diagnosticProperties = ImmutableDictionary.Empty - .Add(CommonInterest.FileNamePatternForMethodsThatAssertMainThread.ToString(), string.Join("\n", mainThreadAssertingMethods)) - .Add(CommonInterest.FileNamePatternForMethodsThatSwitchToMainThread.ToString(), string.Join("\n", mainThreadSwitchingMethods)); - - var methodsDeclaringUIThreadRequirement = new HashSet(); - var methodsAssertingUIThreadRequirement = new HashSet(); - var callerToCalleeMap = new Dictionary>(); - - compilationStartContext.RegisterCodeBlockStartAction(codeBlockStartContext => + Dictionary>? calleeToCallerMap = CreateCalleeToCallerMap(callerToCalleeMap); + HashSet? transitiveClosureOfMainThreadRequiringMethods = GetTransitiveClosureOfMainThreadRequiringMethods(methodsAssertingUIThreadRequirement, calleeToCallerMap); + foreach (IMethodSymbol? implicitUserMethod in transitiveClosureOfMainThreadRequiringMethods.Except(methodsDeclaringUIThreadRequirement)) { - var methodAnalyzer = new MethodAnalyzer( - mainThreadAssertingMethods: mainThreadAssertingMethods, - mainThreadSwitchingMethods: mainThreadSwitchingMethods, - membersRequiringMainThread: membersRequiringMainThread, - methodsDeclaringUIThreadRequirement: methodsDeclaringUIThreadRequirement, - methodsAssertingUIThreadRequirement: methodsAssertingUIThreadRequirement, - diagnosticProperties: diagnosticProperties); - codeBlockStartContext.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzeInvocation), SyntaxKind.InvocationExpression); - codeBlockStartContext.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzeMemberAccess), SyntaxKind.SimpleMemberAccessExpression); - codeBlockStartContext.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzeCast), SyntaxKind.CastExpression); - codeBlockStartContext.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzeAs), SyntaxKind.AsExpression); - codeBlockStartContext.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzeAs), SyntaxKind.IsExpression); - codeBlockStartContext.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzeIsPattern), SyntaxKind.IsPatternExpression); - }); - - compilationStartContext.RegisterOperationAction(Utils.DebuggableWrapper(c => this.AddToCallerCalleeMap(c, callerToCalleeMap)), OperationKind.Invocation); - compilationStartContext.RegisterOperationAction(Utils.DebuggableWrapper(c => this.AddToCallerCalleeMap(c, callerToCalleeMap)), OperationKind.PropertyReference); - - // Strictly speaking, this will miss access to the underlying field, but there's no method to put in the map in that case - compilationStartContext.RegisterOperationAction(Utils.DebuggableWrapper(c => this.AddToCallerCalleeMap(c, callerToCalleeMap)), OperationKind.EventAssignment); - - compilationStartContext.RegisterCompilationEndAction(compilationEndContext => - { - Dictionary>? calleeToCallerMap = CreateCalleeToCallerMap(callerToCalleeMap); - HashSet? transitiveClosureOfMainThreadRequiringMethods = GetTransitiveClosureOfMainThreadRequiringMethods(methodsAssertingUIThreadRequirement, calleeToCallerMap); - foreach (IMethodSymbol? implicitUserMethod in transitiveClosureOfMainThreadRequiringMethods.Except(methodsDeclaringUIThreadRequirement)) + var reportSites = callerToCalleeMap[implicitUserMethod] + .Where(info => transitiveClosureOfMainThreadRequiringMethods.Contains(info.MethodSymbol)) + .GroupBy(info => info.MethodSymbol, SymbolEqualityComparer.Default) + .Select(bySymbol => new { Location = bySymbol.First().InvocationSyntax.GetLocation(), CalleeMethod = bySymbol.Key }); + foreach (var site in reportSites) { - var reportSites = from info in callerToCalleeMap[implicitUserMethod] - where transitiveClosureOfMainThreadRequiringMethods.Contains(info.MethodSymbol) - group info by info.MethodSymbol into bySymbol - select new { Location = bySymbol.First().InvocationSyntax.GetLocation(), CalleeMethod = bySymbol.Key }; - foreach (var site in reportSites) - { - bool isAsync = Utils.IsAsyncReady(implicitUserMethod); - DiagnosticDescriptor? descriptor = isAsync ? DescriptorAsync : DescriptorSync; - string calleeName = Utils.GetFullName(site.CalleeMethod); - var formattingArgs = isAsync ? new object[] { calleeName } : new object[] { calleeName, mainThreadAssertingMethods.FirstOrDefault() }; - Diagnostic diagnostic = Diagnostic.Create( - descriptor, - site.Location, - diagnosticProperties, - formattingArgs); - compilationEndContext.ReportDiagnostic(diagnostic); - } + bool isAsync = Utils.IsAsyncReady(implicitUserMethod); + DiagnosticDescriptor? descriptor = isAsync ? DescriptorAsync : DescriptorSync; + string calleeName = Utils.GetFullName(site.CalleeMethod); + var formattingArgs = isAsync ? new object[] { calleeName } : new object[] { calleeName, mainThreadAssertingMethods.FirstOrDefault() }; + Diagnostic diagnostic = Diagnostic.Create( + descriptor, + site.Location, + diagnosticProperties, + formattingArgs); + compilationEndContext.ReportDiagnostic(diagnostic); } - }); + } }); - } + }); + } - private static HashSet GetTransitiveClosureOfMainThreadRequiringMethods(HashSet methodsRequiringUIThread, Dictionary> calleeToCallerMap) - { - var result = new HashSet(); + private static HashSet GetTransitiveClosureOfMainThreadRequiringMethods(HashSet methodsRequiringUIThread, Dictionary> calleeToCallerMap) + { + var result = new HashSet(SymbolEqualityComparer.Default); - void MarkMethod(IMethodSymbol method) + void MarkMethod(IMethodSymbol method) + { + if (result.Add(method) && calleeToCallerMap.TryGetValue(method, out List? callers)) { - if (result.Add(method) && calleeToCallerMap.TryGetValue(method, out List? callers)) + // If this is an async method, do *not* propagate its thread affinity to its callers. + if (!Utils.IsAsyncCompatibleReturnType(method.ReturnType)) { - // If this is an async method, do *not* propagate its thread affinity to its callers. - if (!Utils.IsAsyncCompatibleReturnType(method.ReturnType)) + foreach (CallInfo caller in callers) { - foreach (CallInfo caller in callers) - { - MarkMethod(caller.MethodSymbol); - } + MarkMethod(caller.MethodSymbol); } } } - - foreach (IMethodSymbol? method in methodsRequiringUIThread) - { - MarkMethod(method); - } - - return result; } - private static Dictionary> CreateCalleeToCallerMap(Dictionary> callerToCalleeMap) + foreach (IMethodSymbol? method in methodsRequiringUIThread) { - var result = new Dictionary>(); + MarkMethod(method); + } - foreach (KeyValuePair> item in callerToCalleeMap) + return result; + } + + private static Dictionary> CreateCalleeToCallerMap(Dictionary> callerToCalleeMap) + { + var result = new Dictionary>(SymbolEqualityComparer.Default); + + foreach (KeyValuePair> item in callerToCalleeMap) + { + IMethodSymbol? caller = item.Key; + foreach (CallInfo callee in item.Value) { - IMethodSymbol? caller = item.Key; - foreach (CallInfo callee in item.Value) + if (!result.TryGetValue(callee.MethodSymbol, out List? callers)) { - if (!result.TryGetValue(callee.MethodSymbol, out List? callers)) - { - result[callee.MethodSymbol] = callers = new List(); - } - - callers.Add(new CallInfo(methodSymbol: caller, callee.InvocationSyntax)); + result[callee.MethodSymbol] = callers = new List(); } + + callers.Add(new CallInfo(methodSymbol: caller, callee.InvocationSyntax)); } + } + + return result; + } - return result; + private void AddToCallerCalleeMap(OperationAnalysisContext context, Dictionary> callerToCalleeMap) + { + if (CSharpUtils.IsWithinNameOf(context.Operation.Syntax)) + { + return; } - private void AddToCallerCalleeMap(OperationAnalysisContext context, Dictionary> callerToCalleeMap) + IMethodSymbol? GetPropertyAccessor(IPropertySymbol? propertySymbol) { - if (CSharpUtils.IsWithinNameOf(context.Operation.Syntax)) + if (propertySymbol is object) { - return; + return CSharpUtils.IsOnLeftHandOfAssignment(context.Operation.Syntax) + ? propertySymbol.SetMethod + : propertySymbol.GetMethod; } - IMethodSymbol? GetPropertyAccessor(IPropertySymbol? propertySymbol) - { - if (propertySymbol is object) + return null; + } + + ISymbol? targetMethod = null; + SyntaxNode locationToBlame = context.Operation.Syntax; + switch (context.Operation) + { + case IInvocationOperation invocationOperation: + targetMethod = invocationOperation.TargetMethod; + locationToBlame = this.languageUtils.IsolateMethodName(invocationOperation); + break; + case IPropertyReferenceOperation propertyReference: + targetMethod = GetPropertyAccessor(propertyReference.Property); + break; + case IEventAssignmentOperation eventAssignmentOperation: + IOperation eventReferenceOp = eventAssignmentOperation.EventReference; + if (eventReferenceOp is IEventReferenceOperation eventReference) { - return CSharpUtils.IsOnLeftHandOfAssignment(context.Operation.Syntax) - ? propertySymbol.SetMethod - : propertySymbol.GetMethod; + targetMethod = eventAssignmentOperation.Adds + ? eventReference.Event.AddMethod + : eventReference.Event.RemoveMethod; + locationToBlame = eventReference.Syntax; } - return null; - } - - ISymbol? targetMethod = null; - SyntaxNode locationToBlame = context.Operation.Syntax; - switch (context.Operation) - { - case IInvocationOperation invocationOperation: - targetMethod = invocationOperation.TargetMethod; - locationToBlame = this.languageUtils.IsolateMethodName(invocationOperation); - break; - case IPropertyReferenceOperation propertyReference: - targetMethod = GetPropertyAccessor(propertyReference.Property); - break; - case IEventAssignmentOperation eventAssignmentOperation: - IOperation eventReferenceOp = eventAssignmentOperation.EventReference; - if (eventReferenceOp is IEventReferenceOperation eventReference) - { - targetMethod = eventAssignmentOperation.Adds - ? eventReference.Event.AddMethod - : eventReference.Event.RemoveMethod; - locationToBlame = eventReference.Syntax; - } - - break; - } + break; + } - if (context.ContainingSymbol is IMethodSymbol caller && targetMethod is IMethodSymbol callee) + if (context.ContainingSymbol is IMethodSymbol caller && targetMethod is IMethodSymbol callee) + { + lock (callerToCalleeMap) { - lock (callerToCalleeMap) + if (!callerToCalleeMap.TryGetValue(caller, out List callees)) { - if (!callerToCalleeMap.TryGetValue(caller, out List callees)) - { - callerToCalleeMap[caller] = callees = new List(); - } - - callees.Add(new CallInfo(methodSymbol: callee, invocationSyntax: locationToBlame)); + callerToCalleeMap[caller] = callees = new List(); } + + callees.Add(new CallInfo(methodSymbol: callee, invocationSyntax: locationToBlame)); } } + } - private readonly struct CallInfo + private readonly struct CallInfo + { + public CallInfo(IMethodSymbol methodSymbol, SyntaxNode invocationSyntax) { - public CallInfo(IMethodSymbol methodSymbol, SyntaxNode invocationSyntax) - { - this.MethodSymbol = methodSymbol; - this.InvocationSyntax = invocationSyntax; - } + this.MethodSymbol = methodSymbol; + this.InvocationSyntax = invocationSyntax; + } - public IMethodSymbol MethodSymbol { get; } + public IMethodSymbol MethodSymbol { get; } - public SyntaxNode InvocationSyntax { get; } - } + public SyntaxNode InvocationSyntax { get; } + } - private class MethodAnalyzer + private class MethodAnalyzer + { + private ImmutableDictionary methodDeclarationNodes = ImmutableDictionary.Empty; + + public MethodAnalyzer( + ImmutableArray mainThreadAssertingMethods, + ImmutableArray mainThreadSwitchingMethods, + ImmutableArray membersRequiringMainThread, + HashSet methodsDeclaringUIThreadRequirement, + HashSet methodsAssertingUIThreadRequirement, + ImmutableDictionary diagnosticProperties) { - private ImmutableDictionary methodDeclarationNodes = ImmutableDictionary.Empty; - - public MethodAnalyzer( - ImmutableArray mainThreadAssertingMethods, - ImmutableArray mainThreadSwitchingMethods, - ImmutableArray membersRequiringMainThread, - HashSet methodsDeclaringUIThreadRequirement, - HashSet methodsAssertingUIThreadRequirement, - ImmutableDictionary diagnosticProperties) - { - this.MainThreadAssertingMethods = mainThreadAssertingMethods; - this.MainThreadSwitchingMethods = mainThreadSwitchingMethods; - this.MembersRequiringMainThread = membersRequiringMainThread; - this.MethodsDeclaringUIThreadRequirement = methodsDeclaringUIThreadRequirement; - this.MethodsAssertingUIThreadRequirement = methodsAssertingUIThreadRequirement; - this.DiagnosticProperties = diagnosticProperties; - } + this.MainThreadAssertingMethods = mainThreadAssertingMethods; + this.MainThreadSwitchingMethods = mainThreadSwitchingMethods; + this.MembersRequiringMainThread = membersRequiringMainThread; + this.MethodsDeclaringUIThreadRequirement = methodsDeclaringUIThreadRequirement; + this.MethodsAssertingUIThreadRequirement = methodsAssertingUIThreadRequirement; + this.DiagnosticProperties = diagnosticProperties; + } - internal ImmutableArray MainThreadAssertingMethods { get; } + internal ImmutableArray MainThreadAssertingMethods { get; } - internal ImmutableArray MainThreadSwitchingMethods { get; } + internal ImmutableArray MainThreadSwitchingMethods { get; } - internal ImmutableArray MembersRequiringMainThread { get; } + internal ImmutableArray MembersRequiringMainThread { get; } - internal HashSet MethodsDeclaringUIThreadRequirement { get; } + internal HashSet MethodsDeclaringUIThreadRequirement { get; } - internal HashSet MethodsAssertingUIThreadRequirement { get; } + internal HashSet MethodsAssertingUIThreadRequirement { get; } - internal ImmutableDictionary DiagnosticProperties { get; } + internal ImmutableDictionary DiagnosticProperties { get; } - internal void AnalyzeInvocation(SyntaxNodeAnalysisContext context) + internal void AnalyzeInvocation(SyntaxNodeAnalysisContext context) + { + var invocationSyntax = (InvocationExpressionSyntax)context.Node; + var invokedMethod = context.SemanticModel.GetSymbolInfo(context.Node).Symbol as IMethodSymbol; + if (invokedMethod is object) { - var invocationSyntax = (InvocationExpressionSyntax)context.Node; - var invokedMethod = context.SemanticModel.GetSymbolInfo(context.Node).Symbol as IMethodSymbol; - if (invokedMethod is object) + SyntaxNode? methodDeclaration = context.Node.FirstAncestorOrSelf(n => CSharpCommonInterest.MethodSyntaxKinds.Contains(n.Kind())); + if (methodDeclaration is object) { - SyntaxNode? methodDeclaration = context.Node.FirstAncestorOrSelf(n => CSharpCommonInterest.MethodSyntaxKinds.Contains(n.Kind())); - if (methodDeclaration is object) + bool assertsMainThread = this.MainThreadAssertingMethods.Contains(invokedMethod); + bool switchesToMainThread = this.MainThreadSwitchingMethods.Contains(invokedMethod); + if (assertsMainThread || switchesToMainThread) { - bool assertsMainThread = this.MainThreadAssertingMethods.Contains(invokedMethod); - bool switchesToMainThread = this.MainThreadSwitchingMethods.Contains(invokedMethod); - if (assertsMainThread || switchesToMainThread) + if (context.ContainingSymbol is IMethodSymbol callingMethod) { - if (context.ContainingSymbol is IMethodSymbol callingMethod) + lock (this.MethodsDeclaringUIThreadRequirement) { - lock (this.MethodsDeclaringUIThreadRequirement) - { - this.MethodsDeclaringUIThreadRequirement.Add(callingMethod); - } + this.MethodsDeclaringUIThreadRequirement.Add(callingMethod); + } - if (assertsMainThread) + if (assertsMainThread) + { + lock (this.MethodsAssertingUIThreadRequirement) { - lock (this.MethodsAssertingUIThreadRequirement) - { - this.MethodsAssertingUIThreadRequirement.Add(callingMethod); - } + this.MethodsAssertingUIThreadRequirement.Add(callingMethod); } } - - this.methodDeclarationNodes = this.methodDeclarationNodes.SetItem(methodDeclaration, ThreadingContext.MainThread); - return; } + + this.methodDeclarationNodes = this.methodDeclarationNodes.SetItem(methodDeclaration, ThreadingContext.MainThread); + return; } + } - // The diagnostic (if any) should underline the method name only. - ExpressionSyntax? focusedNode = invocationSyntax.Expression; - focusedNode = (focusedNode as MemberAccessExpressionSyntax)?.Name ?? focusedNode; - if (!this.AnalyzeMemberWithinContext(invokedMethod.ContainingType, invokedMethod, context, focusedNode.GetLocation())) + // The diagnostic (if any) should underline the method name only. + ExpressionSyntax? focusedNode = invocationSyntax.Expression; + focusedNode = (focusedNode as MemberAccessExpressionSyntax)?.Name ?? focusedNode; + if (!this.AnalyzeMemberWithinContext(invokedMethod.ContainingType, invokedMethod, context, focusedNode.GetLocation())) + { + foreach (ITypeSymbol? iface in invokedMethod.FindInterfacesImplemented()) { - foreach (ITypeSymbol? iface in invokedMethod.FindInterfacesImplemented()) + if (this.AnalyzeMemberWithinContext(iface, invokedMethod, context, focusedNode.GetLocation())) { - if (this.AnalyzeMemberWithinContext(iface, invokedMethod, context, focusedNode.GetLocation())) - { - // Just report the first diagnostic. - break; - } + // Just report the first diagnostic. + break; } } } } + } - internal void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context) + internal void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context) + { + var memberAccessSyntax = (MemberAccessExpressionSyntax)context.Node; + var property = context.SemanticModel.GetSymbolInfo(context.Node).Symbol as IPropertySymbol; + if (property is object) { - var memberAccessSyntax = (MemberAccessExpressionSyntax)context.Node; - var property = context.SemanticModel.GetSymbolInfo(context.Node).Symbol as IPropertySymbol; - if (property is object) - { - this.AnalyzeMemberWithinContext(property.ContainingType, property, context, memberAccessSyntax.Name.GetLocation()); - } - else + this.AnalyzeMemberWithinContext(property.ContainingType, property, context, memberAccessSyntax.Name.GetLocation()); + } + else + { + var @event = context.SemanticModel.GetSymbolInfo(context.Node).Symbol as IEventSymbol; + if (@event is object) { - var @event = context.SemanticModel.GetSymbolInfo(context.Node).Symbol as IEventSymbol; - if (@event is object) - { - this.AnalyzeMemberWithinContext(@event.ContainingType, @event, context, memberAccessSyntax.Name.GetLocation()); - } + this.AnalyzeMemberWithinContext(@event.ContainingType, @event, context, memberAccessSyntax.Name.GetLocation()); } } + } - internal void AnalyzeCast(SyntaxNodeAnalysisContext context) + internal void AnalyzeCast(SyntaxNodeAnalysisContext context) + { + var castSyntax = (CastExpressionSyntax)context.Node; + var type = context.SemanticModel.GetSymbolInfo(castSyntax.Type, context.CancellationToken).Symbol as ITypeSymbol; + if (type is object && IsObjectLikelyToBeCOMObject(type)) { - var castSyntax = (CastExpressionSyntax)context.Node; - var type = context.SemanticModel.GetSymbolInfo(castSyntax.Type, context.CancellationToken).Symbol as ITypeSymbol; - if (type is object && IsObjectLikelyToBeCOMObject(type)) - { - this.AnalyzeMemberWithinContext(type, null, context); - } + this.AnalyzeMemberWithinContext(type, null, context); } + } - internal void AnalyzeAs(SyntaxNodeAnalysisContext context) + internal void AnalyzeAs(SyntaxNodeAnalysisContext context) + { + var asSyntax = (BinaryExpressionSyntax)context.Node; + var type = context.SemanticModel.GetSymbolInfo(asSyntax.Right, context.CancellationToken).Symbol as ITypeSymbol; + if (type is object && IsObjectLikelyToBeCOMObject(type)) { - var asSyntax = (BinaryExpressionSyntax)context.Node; - var type = context.SemanticModel.GetSymbolInfo(asSyntax.Right, context.CancellationToken).Symbol as ITypeSymbol; - if (type is object && IsObjectLikelyToBeCOMObject(type)) - { - Location asAndRightSide = Location.Create(context.Node.SyntaxTree, TextSpan.FromBounds(asSyntax.OperatorToken.Span.Start, asSyntax.Right.Span.End)); - this.AnalyzeMemberWithinContext(type, null, context, asAndRightSide); - } + Location asAndRightSide = Location.Create(context.Node.SyntaxTree, TextSpan.FromBounds(asSyntax.OperatorToken.Span.Start, asSyntax.Right.Span.End)); + this.AnalyzeMemberWithinContext(type, null, context, asAndRightSide); } + } - internal void AnalyzeIsPattern(SyntaxNodeAnalysisContext context) + internal void AnalyzeIsPattern(SyntaxNodeAnalysisContext context) + { + var patternSyntax = (IsPatternExpressionSyntax)context.Node; + if (patternSyntax.Pattern is DeclarationPatternSyntax declarationPatternSyntax && declarationPatternSyntax.Type is object) { - var patternSyntax = (IsPatternExpressionSyntax)context.Node; - if (patternSyntax.Pattern is DeclarationPatternSyntax declarationPatternSyntax && declarationPatternSyntax.Type is object) + var type = context.SemanticModel.GetSymbolInfo(declarationPatternSyntax.Type, context.CancellationToken).Symbol as ITypeSymbol; + if (type is object && IsObjectLikelyToBeCOMObject(type)) { - var type = context.SemanticModel.GetSymbolInfo(declarationPatternSyntax.Type, context.CancellationToken).Symbol as ITypeSymbol; - if (type is object && IsObjectLikelyToBeCOMObject(type)) - { - Location isAndTypeSide = Location.Create( - context.Node.SyntaxTree, - TextSpan.FromBounds( - patternSyntax.IsKeyword.SpanStart, - declarationPatternSyntax.Type.Span.End)); - this.AnalyzeMemberWithinContext(type, null, context, isAndTypeSide); - } + Location isAndTypeSide = Location.Create( + context.Node.SyntaxTree, + TextSpan.FromBounds( + patternSyntax.IsKeyword.SpanStart, + declarationPatternSyntax.Type.Span.End)); + this.AnalyzeMemberWithinContext(type, null, context, isAndTypeSide); } } + } - /// - /// Determines whether a given type is likely to be (or implemented by) a COM object. - /// - /// true if the type appears to be a COM object; false if a managed object. - /// - /// Type casts and type checks are thread-affinitized for (STA) COM objects, and free-threaded for managed ones. - /// - private static bool IsObjectLikelyToBeCOMObject(ITypeSymbol typeSymbol) + /// + /// Determines whether a given type is likely to be (or implemented by) a COM object. + /// + /// if the type appears to be a COM object; if a managed object. + /// + /// Type casts and type checks are thread-affinitized for (STA) COM objects, and free-threaded for managed ones. + /// + private static bool IsObjectLikelyToBeCOMObject(ITypeSymbol typeSymbol) + { + if (typeSymbol is null) { - if (typeSymbol is null) - { - throw new ArgumentNullException(nameof(typeSymbol)); - } + throw new ArgumentNullException(nameof(typeSymbol)); + } + + return typeSymbol.GetAttributes().Any(ad => + (ad.AttributeClass?.Name == Types.CoClassAttribute.TypeName && ad.AttributeClass.BelongsToNamespace(Types.CoClassAttribute.Namespace)) || + (ad.AttributeClass?.Name == Types.ComImportAttribute.TypeName && ad.AttributeClass.BelongsToNamespace(Types.ComImportAttribute.Namespace)) || + (ad.AttributeClass?.Name == Types.InterfaceTypeAttribute.TypeName && ad.AttributeClass.BelongsToNamespace(Types.InterfaceTypeAttribute.Namespace)) || + (ad.AttributeClass?.Name == Types.TypeLibTypeAttribute.TypeName && ad.AttributeClass.BelongsToNamespace(Types.TypeLibTypeAttribute.Namespace))); + } - return typeSymbol.GetAttributes().Any(ad => - (ad.AttributeClass.Name == Types.CoClassAttribute.TypeName && ad.AttributeClass.BelongsToNamespace(Types.CoClassAttribute.Namespace)) || - (ad.AttributeClass.Name == Types.ComImportAttribute.TypeName && ad.AttributeClass.BelongsToNamespace(Types.ComImportAttribute.Namespace)) || - (ad.AttributeClass.Name == Types.InterfaceTypeAttribute.TypeName && ad.AttributeClass.BelongsToNamespace(Types.InterfaceTypeAttribute.Namespace)) || - (ad.AttributeClass.Name == Types.TypeLibTypeAttribute.TypeName && ad.AttributeClass.BelongsToNamespace(Types.TypeLibTypeAttribute.Namespace))); + private bool AnalyzeMemberWithinContext(ITypeSymbol type, ISymbol? symbol, SyntaxNodeAnalysisContext context, Location? focusDiagnosticOn = null) + { + if (type is null) + { + throw new ArgumentNullException(nameof(type)); } - private bool AnalyzeMemberWithinContext(ITypeSymbol type, ISymbol? symbol, SyntaxNodeAnalysisContext context, Location? focusDiagnosticOn = null) + bool requiresUIThread = (type.TypeKind == TypeKind.Interface || type.TypeKind == TypeKind.Class || type.TypeKind == TypeKind.Struct) + && this.MembersRequiringMainThread.Contains(type, symbol); + + if (requiresUIThread) { - if (type is null) + ThreadingContext threadingContext = ThreadingContext.Unknown; + SyntaxNode? methodDeclaration = context.Node.FirstAncestorOrSelf(n => CSharpCommonInterest.MethodSyntaxKinds.Contains(n.Kind())); + if (methodDeclaration is object) { - throw new ArgumentNullException(nameof(type)); + threadingContext = this.methodDeclarationNodes.GetValueOrDefault(methodDeclaration); } - bool requiresUIThread = (type.TypeKind == TypeKind.Interface || type.TypeKind == TypeKind.Class || type.TypeKind == TypeKind.Struct) - && this.MembersRequiringMainThread.Contains(type, symbol); - - if (requiresUIThread) + if (threadingContext != ThreadingContext.MainThread) { - ThreadingContext threadingContext = ThreadingContext.Unknown; - SyntaxNode? methodDeclaration = context.Node.FirstAncestorOrSelf(n => CSharpCommonInterest.MethodSyntaxKinds.Contains(n.Kind())); - if (methodDeclaration is object) - { - threadingContext = this.methodDeclarationNodes.GetValueOrDefault(methodDeclaration); - } - - if (threadingContext != ThreadingContext.MainThread) - { - CSharpUtils.ContainingFunctionData function = CSharpUtils.GetContainingFunction((CSharpSyntaxNode)context.Node); - Location location = focusDiagnosticOn ?? context.Node.GetLocation(); - DiagnosticDescriptor? descriptor = function.IsAsync ? DescriptorAsync : DescriptorSync; - var formattingArgs = function.IsAsync ? new object[] { type.Name } : new object[] { type.Name, this.MainThreadAssertingMethods.FirstOrDefault() }; - context.ReportDiagnostic(Diagnostic.Create(descriptor, location, this.DiagnosticProperties, formattingArgs)); - return true; - } + CSharpUtils.ContainingFunctionData function = CSharpUtils.GetContainingFunction((CSharpSyntaxNode)context.Node); + Location location = focusDiagnosticOn ?? context.Node.GetLocation(); + DiagnosticDescriptor? descriptor = function.IsAsync ? DescriptorAsync : DescriptorSync; + var formattingArgs = function.IsAsync ? new object[] { type.Name } : new object[] { type.Name, this.MainThreadAssertingMethods.FirstOrDefault() }; + context.ReportDiagnostic(Diagnostic.Create(descriptor, location, this.DiagnosticProperties, formattingArgs)); + return true; } - - return false; } + + return false; } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD102AvoidJtfRunInNonPublicMembersAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD102AvoidJtfRunInNonPublicMembersAnalyzer.cs index 88a35955d..6f9b79c0d 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD102AvoidJtfRunInNonPublicMembersAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD102AvoidJtfRunInNonPublicMembersAnalyzer.cs @@ -1,73 +1,72 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Linq; - using System.Text; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CSharp; - using Microsoft.CodeAnalysis.CSharp.Syntax; - using Microsoft.CodeAnalysis.Diagnostics; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; - /// - /// Discourages use of JTF.Run except in public members where the author presumably - /// has limited opportunity to make the method async due to API impact and breaking changes. - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public class VSTHRD102AvoidJtfRunInNonPublicMembersAnalyzer : DiagnosticAnalyzer - { - public const string Id = "VSTHRD102"; +namespace Microsoft.VisualStudio.Threading.Analyzers; - internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD102_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD102_MessageFormat), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Info, - isEnabledByDefault: true); +/// +/// Discourages use of JTF.Run except in public members where the author presumably +/// has limited opportunity to make the method async due to API impact and breaking changes. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class VSTHRD102AvoidJtfRunInNonPublicMembersAnalyzer : DiagnosticAnalyzer +{ + public const string Id = "VSTHRD102"; - /// - public override ImmutableArray SupportedDiagnostics - { - get { return ImmutableArray.Create(Descriptor); } - } + internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD102_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD102_MessageFormat), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true); - /// - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + /// + public override ImmutableArray SupportedDiagnostics + { + get { return ImmutableArray.Create(Descriptor); } + } - context.RegisterCodeBlockStartAction(ctxt => - { - // We want to scan invocations that occur inside internal, synchronous methods - // for calls to JTF.Run or JT.Join. - var methodSymbol = ctxt.OwningSymbol as IMethodSymbol; - if (!methodSymbol.HasAsyncCompatibleReturnType() && !Utils.IsPublic(methodSymbol) && !Utils.IsEntrypointMethod(methodSymbol, ctxt.SemanticModel, ctxt.CancellationToken) && !methodSymbol.FindInterfacesImplemented().Any(Utils.IsPublic)) - { - ctxt.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(MethodAnalyzer.AnalyzeInvocation), SyntaxKind.InvocationExpression); - } - }); - } + /// + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); - private static class MethodAnalyzer + context.RegisterCodeBlockStartAction(ctxt => { - internal static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) + // We want to scan invocations that occur inside internal, synchronous methods + // for calls to JTF.Run or JT.Join. + var methodSymbol = ctxt.OwningSymbol as IMethodSymbol; + if (!methodSymbol.HasAsyncCompatibleReturnType() && !Utils.IsPublic(methodSymbol) && !Utils.IsEntrypointMethod(methodSymbol, ctxt.SemanticModel, ctxt.CancellationToken) && !methodSymbol.FindInterfacesImplemented().Any(Utils.IsPublic)) { - var invocationExpressionSyntax = (InvocationExpressionSyntax)context.Node; - CSharpCommonInterest.InspectMemberAccess( - context, - invocationExpressionSyntax.Expression as MemberAccessExpressionSyntax, - Descriptor, - CommonInterest.JTFSyncBlockers, - ignoreIfInsideAnonymousDelegate: true); + ctxt.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(MethodAnalyzer.AnalyzeInvocation), SyntaxKind.InvocationExpression); } + }); + } + + private static class MethodAnalyzer + { + internal static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) + { + var invocationExpressionSyntax = (InvocationExpressionSyntax)context.Node; + CSharpCommonInterest.InspectMemberAccess( + context, + invocationExpressionSyntax.Expression as MemberAccessExpressionSyntax, + Descriptor, + CommonInterest.JTFSyncBlockers, + ignoreIfInsideAnonymousDelegate: true); } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD103UseAsyncOptionAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD103UseAsyncOptionAnalyzer.cs index 48814ae67..2578f59b8 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD103UseAsyncOptionAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD103UseAsyncOptionAnalyzer.cs @@ -1,220 +1,277 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +/// +/// This analyzer recognizes invocations of JoinableTaskFactory.Run(Func{Task}), JoinableTask.Join(), and variants +/// that occur within an async method, thus defeating a perfect opportunity to be asynchronous. +/// +/// +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class VSTHRD103UseAsyncOptionAnalyzer : DiagnosticAnalyzer { - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Diagnostics.CodeAnalysis; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CSharp; - using Microsoft.CodeAnalysis.CSharp.Syntax; - using Microsoft.CodeAnalysis.Diagnostics; - - /// - /// This analyzer recognizes invocations of JoinableTaskFactory.Run(Func{Task}), JoinableTask.Join(), and variants - /// that occur within an async method, thus defeating a perfect opportunity to be asynchronous. - /// - /// - /// - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public class VSTHRD103UseAsyncOptionAnalyzer : DiagnosticAnalyzer + public const string Id = "VSTHRD103"; + + public const string AsyncMethodKeyName = "AsyncMethodName"; + + public const string ExtensionMethodNamespaceKeyName = "ExtensionMethodNamespace"; + + public static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD103_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD103_MessageFormat), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor DescriptorNoAlternativeMethod = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD103_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD103_MessageFormat_UseAwaitInstead), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + Descriptor, + DescriptorNoAlternativeMethod); + + /// + public override void Initialize(AnalysisContext context) { - public const string Id = "VSTHRD103"; - - internal const string AsyncMethodKeyName = "AsyncMethodName"; - - internal const string ExtensionMethodNamespaceKeyName = "ExtensionMethodNamespace"; - - internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD103_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD103_MessageFormat), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); - - internal static readonly DiagnosticDescriptor DescriptorNoAlternativeMethod = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD103_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD103_MessageFormat_UseAwaitInstead), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); - - /// - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( - Descriptor, - DescriptorNoAlternativeMethod); - - /// - public override void Initialize(AnalysisContext context) + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + + context.RegisterCompilationStartAction(compilationStartContext => { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + var excludedMethods = CommonInterest.ReadMethods(compilationStartContext.Options, CommonInterest.FileNamePatternForSyncMethodsToExcludeFromVSTHRD103, compilationStartContext.CancellationToken).ToImmutableArray(); - context.RegisterCodeBlockStartAction(ctxt => + compilationStartContext.RegisterCodeBlockStartAction(ctxt => { - ctxt.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(MethodAnalyzer.AnalyzeInvocation), SyntaxKind.InvocationExpression); - ctxt.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(MethodAnalyzer.AnalyzePropertyGetter), SyntaxKind.SimpleMemberAccessExpression); + var methodAnalyzer = new MethodAnalyzer(excludedMethods); + ctxt.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzeInvocation), SyntaxKind.InvocationExpression); + ctxt.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzePropertyGetter), SyntaxKind.SimpleMemberAccessExpression); + ctxt.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzeConditionalAccessExpression), SyntaxKind.ConditionalAccessExpression); }); + }); + } + + private class MethodAnalyzer + { + private readonly ImmutableArray excludedMethods; + + public MethodAnalyzer(ImmutableArray excludedMethods) + { + this.excludedMethods = excludedMethods; + } + + internal void AnalyzePropertyGetter(SyntaxNodeAnalysisContext context) + { + var memberAccessSyntax = (MemberAccessExpressionSyntax)context.Node; + if (IsInTaskReturningMethodOrDelegate(context)) + { + this.InspectMemberAccess(context, memberAccessSyntax.Name, CommonInterest.SyncBlockingProperties); + } } - private class MethodAnalyzer + internal void AnalyzeConditionalAccessExpression(SyntaxNodeAnalysisContext context) { - internal static void AnalyzePropertyGetter(SyntaxNodeAnalysisContext context) + var conditionalAccessSyntax = (ConditionalAccessExpressionSyntax)context.Node; + if (IsInTaskReturningMethodOrDelegate(context)) { - var memberAccessSyntax = (MemberAccessExpressionSyntax)context.Node; - if (IsInTaskReturningMethodOrDelegate(context)) + ExpressionSyntax rightSide = conditionalAccessSyntax.WhenNotNull switch { - InspectMemberAccess(context, memberAccessSyntax, CommonInterest.SyncBlockingProperties); - } + MemberBindingExpressionSyntax bindingExpr => bindingExpr.Name, + _ => conditionalAccessSyntax.WhenNotNull, + }; + this.InspectMemberAccess(context, rightSide, CommonInterest.SyncBlockingProperties); } + } - internal static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) + internal void AnalyzeInvocation(SyntaxNodeAnalysisContext context) + { + if (IsInTaskReturningMethodOrDelegate(context)) { - if (IsInTaskReturningMethodOrDelegate(context)) + var invocationExpressionSyntax = (InvocationExpressionSyntax)context.Node; + var memberAccessSyntax = invocationExpressionSyntax.Expression as MemberAccessExpressionSyntax; + if (memberAccessSyntax is not null && this.InspectMemberAccess(context, memberAccessSyntax.Name, CommonInterest.SyncBlockingMethods)) { - var invocationExpressionSyntax = (InvocationExpressionSyntax)context.Node; - var memberAccessSyntax = invocationExpressionSyntax.Expression as MemberAccessExpressionSyntax; - if (InspectMemberAccess(context, memberAccessSyntax, CommonInterest.SyncBlockingMethods)) - { - // Don't return double-diagnostics. - return; - } + // Don't return double-diagnostics. + return; + } + + // Also consider all method calls to check for Async-suffixed alternatives. + SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo(invocationExpressionSyntax, context.CancellationToken); + if (symbolInfo.Symbol is IMethodSymbol methodSymbol && !methodSymbol.Name.EndsWith(VSTHRD200UseAsyncNamingConventionAnalyzer.MandatoryAsyncSuffix, StringComparison.CurrentCulture) && + !methodSymbol.HasAsyncCompatibleReturnType()) + { + string asyncMethodName = methodSymbol.Name + VSTHRD200UseAsyncNamingConventionAnalyzer.MandatoryAsyncSuffix; + + // For reduced extension methods (invoked as instance.Method()), look up the async + // alternative on the receiver type so that extension methods defined in a separate + // static class (but applicable to the receiver) are found via includeReducedExtensionMethods. + // LookupSymbols with the static declaring class as container does not return extension + // methods applicable to the receiver type. + INamespaceOrTypeSymbol lookupContainer = methodSymbol.ReducedFrom is { } reducedFrom && reducedFrom.Parameters.Length > 0 + ? (INamespaceOrTypeSymbol)reducedFrom.Parameters[0].Type + : methodSymbol.ContainingType; - // Also consider all method calls to check for Async-suffixed alternatives. - SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo(invocationExpressionSyntax, context.CancellationToken); - if (symbolInfo.Symbol is IMethodSymbol methodSymbol && !methodSymbol.Name.EndsWith(VSTHRD200UseAsyncNamingConventionAnalyzer.MandatoryAsyncSuffix, StringComparison.CurrentCulture) && - !methodSymbol.HasAsyncCompatibleReturnType()) + ImmutableArray symbols = context.SemanticModel.LookupSymbols( + invocationExpressionSyntax.Expression.GetLocation().SourceSpan.Start, + lookupContainer, + asyncMethodName, + includeReducedExtensionMethods: true); + + MethodDeclarationSyntax? invocationDeclaringMethod = invocationExpressionSyntax.FirstAncestorOrSelf(); + ExpressionSyntax invokedMethodName = CSharpUtils.IsolateMethodName(invocationExpressionSyntax); + foreach (IMethodSymbol m in symbols.OfType()) { - string asyncMethodName = methodSymbol.Name + VSTHRD200UseAsyncNamingConventionAnalyzer.MandatoryAsyncSuffix; - ImmutableArray symbols = context.SemanticModel.LookupSymbols( - invocationExpressionSyntax.Expression.GetLocation().SourceSpan.Start, - methodSymbol.ContainingType, - asyncMethodName, - includeReducedExtensionMethods: true); - - MethodDeclarationSyntax invocationDeclaringMethod = invocationExpressionSyntax.FirstAncestorOrSelf(); - ExpressionSyntax invokedMethodName = CSharpUtils.IsolateMethodName(invocationExpressionSyntax); - foreach (IMethodSymbol m in symbols.OfType()) + if (!m.IsObsolete() + && HasSupersetOfParameterTypes(m, methodSymbol) + && m.Name != invocationDeclaringMethod?.Identifier.Text + && m.HasAsyncCompatibleReturnType()) { - if (!m.IsObsolete() - && HasSupersetOfParameterTypes(m, methodSymbol) - && m.Name != invocationDeclaringMethod?.Identifier.Text - && m.HasAsyncCompatibleReturnType()) + // Check if this method is excluded from VSTHRD103 diagnostics + if (this.excludedMethods.Contains(methodSymbol)) { - // An async alternative exists. - ImmutableDictionary? properties = ImmutableDictionary.Empty - .Add(AsyncMethodKeyName, asyncMethodName); - - Diagnostic diagnostic = Diagnostic.Create( - Descriptor, - invokedMethodName.GetLocation(), - properties, - invokedMethodName.ToString(), - asyncMethodName); - context.ReportDiagnostic(diagnostic); - return; } + + // An async alternative exists. + ImmutableDictionary? properties = ImmutableDictionary.Empty + .Add(AsyncMethodKeyName, asyncMethodName); + + Diagnostic diagnostic = Diagnostic.Create( + Descriptor, + invokedMethodName.GetLocation(), + properties, + invokedMethodName.ToString(), + asyncMethodName); + context.ReportDiagnostic(diagnostic); + + return; } } } } + } - /// - /// Determines whether the given method has parameters to cover all the parameter types in another method. - /// - /// The candidate method. - /// The baseline method. - /// - /// true if has a superset of parameter types found in ; otherwise false. - /// - private static bool HasSupersetOfParameterTypes(IMethodSymbol candidateMethod, IMethodSymbol baselineMethod) + /// + /// Determines whether the given method has parameters to cover all the parameter types in another method. + /// + /// The candidate method. + /// The baseline method. + /// + /// if has a superset of parameter types found in ; otherwise . + /// + private static bool HasSupersetOfParameterTypes(IMethodSymbol candidateMethod, IMethodSymbol baselineMethod) + { + if (baselineMethod.Parameters.Length > candidateMethod.Parameters.Length) { - return candidateMethod.Parameters.All(candidateParameter => baselineMethod.Parameters.Any(baselineParameter => baselineParameter.Type?.Equals(candidateParameter.Type) ?? false)); + return false; } - private static bool IsInTaskReturningMethodOrDelegate(SyntaxNodeAnalysisContext context) + return baselineMethod.Parameters.All(baselineParameter => candidateMethod.Parameters.Any(candidateParameter => baselineParameter.Type?.Equals(candidateParameter.Type, SymbolEqualityComparer.Default) ?? false)); + } + + private static bool IsInTaskReturningMethodOrDelegate(SyntaxNodeAnalysisContext context) + { + // We want to scan invocations that occur inside Task and Task-returning delegates or methods. + // That is: methods that either are or could be made async. + IMethodSymbol? methodSymbol = null; + for (SyntaxNode? focusedNode = context.Node; focusedNode is not null; focusedNode = focusedNode.Parent) { - // We want to scan invocations that occur inside Task and Task-returning delegates or methods. - // That is: methods that either are or could be made async. - IMethodSymbol? methodSymbol = null; - AnonymousFunctionExpressionSyntax? anonymousFunc = context.Node.FirstAncestorOrSelf(); - if (anonymousFunc is object) + switch (focusedNode) { - SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo(anonymousFunc, context.CancellationToken); - methodSymbol = symbolInfo.Symbol as IMethodSymbol; - } - else - { - MethodDeclarationSyntax? methodDecl = context.Node.FirstAncestorOrSelf(); - if (methodDecl is object) - { + case AnonymousFunctionExpressionSyntax anonFunc: + SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo(anonFunc, context.CancellationToken); + methodSymbol = symbolInfo.Symbol as IMethodSymbol; + break; + case LocalFunctionStatementSyntax localFunc: + methodSymbol = context.SemanticModel.GetDeclaredSymbol(localFunc, context.CancellationToken) as IMethodSymbol; + break; + case MethodDeclarationSyntax methodDecl: methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodDecl, context.CancellationToken); - } + break; + default: + // We want to continue iteration of the for loop. + continue; } - return methodSymbol.HasAsyncCompatibleReturnType(); + // We encountered one of our case statements, so whether or not we have a methodSymbol, we shouldn't look further. + break; } - private static bool InspectMemberAccess(SyntaxNodeAnalysisContext context, [NotNullWhen(true)] MemberAccessExpressionSyntax? memberAccessSyntax, IEnumerable problematicMethods) - { - if (memberAccessSyntax is null) - { - return false; - } + return methodSymbol?.HasAsyncCompatibleReturnType() is true; + } - ISymbol? memberSymbol = context.SemanticModel.GetSymbolInfo(memberAccessSyntax, context.CancellationToken).Symbol; - if (memberSymbol is object) + private bool InspectMemberAccess(SyntaxNodeAnalysisContext context, ExpressionSyntax memberName, IEnumerable problematicMethods) + { + ISymbol? memberSymbol = context.SemanticModel.GetSymbolInfo(memberName, context.CancellationToken).Symbol; + if (memberSymbol is object) + { + foreach (CommonInterest.SyncBlockingMethod item in problematicMethods) { - foreach (CommonInterest.SyncBlockingMethod item in problematicMethods) + if (item.Method.IsMatch(memberSymbol)) { - if (item.Method.IsMatch(memberSymbol)) + // Check if this method is excluded from VSTHRD103 diagnostics + if (this.excludedMethods.Contains(memberSymbol)) { - Location? location = memberAccessSyntax.Name.GetLocation(); - ImmutableDictionary? properties = ImmutableDictionary.Empty - .Add(ExtensionMethodNamespaceKeyName, item.ExtensionMethodNamespace is object ? string.Join(".", item.ExtensionMethodNamespace) : string.Empty); - DiagnosticDescriptor descriptor; - var messageArgs = new List(2); - messageArgs.Add(item.Method.Name); - if (item.AsyncAlternativeMethodName is object) - { - properties = properties.Add(AsyncMethodKeyName, item.AsyncAlternativeMethodName); - descriptor = Descriptor; - messageArgs.Add(item.AsyncAlternativeMethodName); - } - else - { - properties = properties.Add(AsyncMethodKeyName, string.Empty); - descriptor = DescriptorNoAlternativeMethod; - } + return false; + } - Diagnostic diagnostic = Diagnostic.Create(descriptor, location, properties, messageArgs.ToArray()); - context.ReportDiagnostic(diagnostic); - return true; + Location? location = memberName.GetLocation(); + ImmutableDictionary? properties = ImmutableDictionary.Empty + .Add(ExtensionMethodNamespaceKeyName, item.ExtensionMethodNamespace is object ? string.Join(".", item.ExtensionMethodNamespace) : string.Empty); + DiagnosticDescriptor descriptor; + var messageArgs = new List(2); + messageArgs.Add(item.Method.Name); + if (item.AsyncAlternativeMethodName is object) + { + properties = properties.Add(AsyncMethodKeyName, item.AsyncAlternativeMethodName); + descriptor = Descriptor; + messageArgs.Add(item.AsyncAlternativeMethodName); + } + else + { + properties = properties.Add(AsyncMethodKeyName, string.Empty); + descriptor = DescriptorNoAlternativeMethod; } + + Diagnostic diagnostic = Diagnostic.Create(descriptor, location, properties, messageArgs.ToArray()); + context.ReportDiagnostic(diagnostic); + return true; } } - - return false; } + + return false; } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD104OfferAsyncOptionAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD104OfferAsyncOptionAnalyzer.cs index 22bbc29bd..c89bf1dab 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD104OfferAsyncOptionAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD104OfferAsyncOptionAnalyzer.cs @@ -1,107 +1,106 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class VSTHRD104OfferAsyncOptionAnalyzer : DiagnosticAnalyzer { - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Linq; - using System.Text; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CSharp; - using Microsoft.CodeAnalysis.CSharp.Syntax; - using Microsoft.CodeAnalysis.Diagnostics; + public const string Id = "VSTHRD104"; - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public class VSTHRD104OfferAsyncOptionAnalyzer : DiagnosticAnalyzer - { - public const string Id = "VSTHRD104"; + internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD104_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD104_MessageFormat), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true); - internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD104_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD104_MessageFormat), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Info, - isEnabledByDefault: true); + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); - public override void Initialize(AnalysisContext context) + context.RegisterCodeBlockStartAction(ctxt => { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); - - context.RegisterCodeBlockStartAction(ctxt => + // We want to scan invocations that occur inside internal, synchronous methods + // for calls to JTF.Run or JT.Join. + var methodSymbol = ctxt.OwningSymbol as IMethodSymbol; + if (!methodSymbol.HasAsyncCompatibleReturnType() && Utils.IsPublic(methodSymbol) && !Utils.IsEntrypointMethod(methodSymbol, ctxt.SemanticModel, ctxt.CancellationToken) && !methodSymbol.HasAsyncAlternative(ctxt.CancellationToken)) { - // We want to scan invocations that occur inside internal, synchronous methods - // for calls to JTF.Run or JT.Join. - var methodSymbol = ctxt.OwningSymbol as IMethodSymbol; - if (!methodSymbol.HasAsyncCompatibleReturnType() && Utils.IsPublic(methodSymbol) && !Utils.IsEntrypointMethod(methodSymbol, ctxt.SemanticModel, ctxt.CancellationToken) && !methodSymbol.HasAsyncAlternative(ctxt.CancellationToken)) - { - var methodAnalyzer = new MethodAnalyzer(); - ctxt.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzeInvocation), SyntaxKind.InvocationExpression); - ctxt.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzePropertyGetter), SyntaxKind.SimpleMemberAccessExpression); - } - }); + var methodAnalyzer = new MethodAnalyzer(); + ctxt.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzeInvocation), SyntaxKind.InvocationExpression); + ctxt.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzePropertyGetter), SyntaxKind.SimpleMemberAccessExpression); + } + }); + } + + private class MethodAnalyzer + { + private bool diagnosticReported; + + internal void AnalyzePropertyGetter(SyntaxNodeAnalysisContext context) + { + var memberAccessSyntax = (MemberAccessExpressionSyntax)context.Node; + this.InspectMemberAccess(context, memberAccessSyntax, CommonInterest.SyncBlockingProperties); } - private class MethodAnalyzer + internal void AnalyzeInvocation(SyntaxNodeAnalysisContext context) { - private bool diagnosticReported; + var invocationExpressionSyntax = (InvocationExpressionSyntax)context.Node; + this.InspectMemberAccess(context, invocationExpressionSyntax.Expression as MemberAccessExpressionSyntax, CommonInterest.JTFSyncBlockers); + } - internal void AnalyzePropertyGetter(SyntaxNodeAnalysisContext context) + private void InspectMemberAccess(SyntaxNodeAnalysisContext context, MemberAccessExpressionSyntax? memberAccessSyntax, IEnumerable problematicMethods) + { + if (memberAccessSyntax is null) { - var memberAccessSyntax = (MemberAccessExpressionSyntax)context.Node; - this.InspectMemberAccess(context, memberAccessSyntax, CommonInterest.SyncBlockingProperties); + return; } - internal void AnalyzeInvocation(SyntaxNodeAnalysisContext context) + if (this.diagnosticReported) { - var invocationExpressionSyntax = (InvocationExpressionSyntax)context.Node; - this.InspectMemberAccess(context, invocationExpressionSyntax.Expression as MemberAccessExpressionSyntax, CommonInterest.JTFSyncBlockers); + // Don't report more than once per method. + return; } - private void InspectMemberAccess(SyntaxNodeAnalysisContext context, MemberAccessExpressionSyntax? memberAccessSyntax, IEnumerable problematicMethods) + if (context.Node.FirstAncestorOrSelf() is object) { - if (memberAccessSyntax is null) - { - return; - } - - if (this.diagnosticReported) - { - // Don't report more than once per method. - return; - } - - if (context.Node.FirstAncestorOrSelf() is object) - { - // We do not analyze JTF.Run inside anonymous functions because - // they are so often used as callbacks where the signature is constrained. - return; - } + // We do not analyze JTF.Run inside anonymous functions because + // they are so often used as callbacks where the signature is constrained. + return; + } - if (CSharpUtils.IsWithinNameOf(context.Node as ExpressionSyntax)) - { - // We do not consider arguments to nameof( ) because they do not represent invocations of code. - return; - } + if (CSharpUtils.IsWithinNameOf(context.Node as ExpressionSyntax)) + { + // We do not consider arguments to nameof( ) because they do not represent invocations of code. + return; + } - ISymbol? invokedMember = context.SemanticModel.GetSymbolInfo(memberAccessSyntax, context.CancellationToken).Symbol; - if (invokedMember is object) + ISymbol? invokedMember = context.SemanticModel.GetSymbolInfo(memberAccessSyntax, context.CancellationToken).Symbol; + if (invokedMember is object) + { + foreach (CommonInterest.SyncBlockingMethod item in problematicMethods) { - foreach (CommonInterest.SyncBlockingMethod item in problematicMethods) + if (item.Method.IsMatch(invokedMember)) { - if (item.Method.IsMatch(invokedMember)) - { - Location? location = memberAccessSyntax.Name.GetLocation(); - context.ReportDiagnostic(Diagnostic.Create(Descriptor, location)); - this.diagnosticReported = true; - } + Location? location = memberAccessSyntax.Name.GetLocation(); + context.ReportDiagnostic(Diagnostic.Create(Descriptor, location)); + this.diagnosticReported = true; } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD107AwaitTaskWithinUsingExpressionAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD107AwaitTaskWithinUsingExpressionAnalyzer.cs index 5980e4590..e214b5057 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD107AwaitTaskWithinUsingExpressionAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD107AwaitTaskWithinUsingExpressionAnalyzer.cs @@ -1,75 +1,74 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CSharp; - using Microsoft.CodeAnalysis.CSharp.Syntax; - using Microsoft.CodeAnalysis.Diagnostics; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; - /// - /// Analyzes expressions of `using` statements and creates a diagnostic when the expression - /// is of type . - /// - /// - /// An example of a flagged issue: - /// - /// - /// - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public class VSTHRD107AwaitTaskWithinUsingExpressionAnalyzer : DiagnosticAnalyzer - { - public const string Id = "VSTHRD107"; +namespace Microsoft.VisualStudio.Threading.Analyzers; - internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD107_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD107_MessageFormat), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); +/// +/// Analyzes expressions of `using` statements and creates a diagnostic when the expression +/// is of type . +/// +/// +/// An example of a flagged issue: +/// +/// +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class VSTHRD107AwaitTaskWithinUsingExpressionAnalyzer : DiagnosticAnalyzer +{ + public const string Id = "VSTHRD107"; - /// - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); + internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD107_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD107_MessageFormat), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); - context.RegisterSyntaxNodeAction( - Utils.DebuggableWrapper(this.AnalyzeNode), - SyntaxKind.UsingStatement); - } + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterSyntaxNodeAction( + Utils.DebuggableWrapper(this.AnalyzeNode), + SyntaxKind.UsingStatement); + } - private void AnalyzeNode(SyntaxNodeAnalysisContext context) + private void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + var usingStatement = (UsingStatementSyntax)context.Node; + if (usingStatement.Expression is object) { - var usingStatement = (UsingStatementSyntax)context.Node; - if (usingStatement.Expression is object) + TypeInfo expressionTypeInfo = context.SemanticModel.GetTypeInfo(usingStatement.Expression, context.CancellationToken); + ITypeSymbol? expressionType = expressionTypeInfo.Type; + if (expressionType?.Name == nameof(Task) && + expressionType.BelongsToNamespace(Namespaces.SystemThreadingTasks)) { - TypeInfo expressionTypeInfo = context.SemanticModel.GetTypeInfo(usingStatement.Expression, context.CancellationToken); - ITypeSymbol expressionType = expressionTypeInfo.Type; - if (expressionType?.Name == nameof(Task) && - expressionType.BelongsToNamespace(Namespaces.SystemThreadingTasks)) - { - context.ReportDiagnostic( - Diagnostic.Create(Descriptor, usingStatement.Expression.GetLocation())); - } + context.ReportDiagnostic( + Diagnostic.Create(Descriptor, usingStatement.Expression.GetLocation())); } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD110ObserveResultOfAsyncCallsAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD110ObserveResultOfAsyncCallsAnalyzer.cs deleted file mode 100644 index 613c2a34c..000000000 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD110ObserveResultOfAsyncCallsAnalyzer.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using System.Collections.Immutable; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CSharp; - using Microsoft.CodeAnalysis.CSharp.Syntax; - using Microsoft.CodeAnalysis.Diagnostics; - - /// - /// Report errors when async methods calls are not awaited or the result used in some way within a synchronous method. - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public class VSTHRD110ObserveResultOfAsyncCallsAnalyzer : DiagnosticAnalyzer - { - public const string Id = "VSTHRD110"; - - internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD110_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD110_MessageFormat), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); - - /// - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); - - /// - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); - - context.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(new PerCompilation().AnalyzeInvocation), SyntaxKind.InvocationExpression); - } - - private class PerCompilation : DiagnosticAnalyzerState - { - internal void AnalyzeInvocation(SyntaxNodeAnalysisContext context) - { - var invocation = (InvocationExpressionSyntax)context.Node; - - // Only consider invocations that are direct statements. Otherwise, we assume their - // result is awaited, assigned, or otherwise consumed. - if (invocation.Parent is ExpressionStatementSyntax || invocation.Parent is ConditionalAccessExpressionSyntax) - { - var methodSymbol = context.SemanticModel.GetSymbolInfo(context.Node).Symbol as IMethodSymbol; - if (this.IsAwaitableType(methodSymbol?.ReturnType, context.Compilation, context.CancellationToken)) - { - if (!CSharpUtils.GetContainingFunction(invocation).IsAsync) - { - Location? location = (CSharpUtils.IsolateMethodName(invocation) ?? invocation.Expression).GetLocation(); - context.ReportDiagnostic(Diagnostic.Create(Descriptor, location)); - } - } - } - } - } - } -} diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/AssemblyInfo.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/AssemblyInfo.cs index 5ab05e470..61f8ea2f6 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/AssemblyInfo.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/AssemblyInfo.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -10,5 +10,3 @@ [assembly: ComVisible(false)] [assembly: NeutralResourcesLanguage("en-US")] - -[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Threading.Analyzers.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/CommonFixes.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/CommonFixes.cs index f0950ae2f..79015ac8d 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/CommonFixes.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/CommonFixes.cs @@ -1,63 +1,62 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; +using static Microsoft.VisualStudio.Threading.Analyzers.CommonInterest; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +internal static class CommonFixes { - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Diagnostics; - using System.IO; - using System.Linq; - using System.Runtime.CompilerServices; - using System.Text.RegularExpressions; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CodeFixes; - using Microsoft.CodeAnalysis.CSharp; - using Microsoft.CodeAnalysis.CSharp.Syntax; - using Microsoft.CodeAnalysis.Diagnostics; - using Microsoft.CodeAnalysis.Text; - using static CommonInterest; - - internal static class CommonFixes + internal static async Task> ReadMethodsAsync(CodeFixContext codeFixContext, Regex fileNamePattern, CancellationToken cancellationToken) { - internal static async Task> ReadMethodsAsync(CodeFixContext codeFixContext, Regex fileNamePattern, CancellationToken cancellationToken) + ImmutableArray.Builder? result = ImmutableArray.CreateBuilder(); + foreach (string line in await ReadAdditionalFilesAsync(codeFixContext.Document.Project.AdditionalDocuments, fileNamePattern, cancellationToken)) { - ImmutableArray.Builder? result = ImmutableArray.CreateBuilder(); - foreach (string line in await ReadAdditionalFilesAsync(codeFixContext.Document.Project.AdditionalDocuments, fileNamePattern, cancellationToken)) - { - result.Add(ParseAdditionalFileMethodLine(line)); - } + result.Add(ParseAdditionalFileMethodLine(line)); + } - return result.ToImmutable(); + return result.ToImmutable(); + } + + internal static async Task> ReadAdditionalFilesAsync(IEnumerable additionalFiles, Regex fileNamePattern, CancellationToken cancellationToken) + { + if (additionalFiles is null) + { + throw new ArgumentNullException(nameof(additionalFiles)); } - internal static async Task> ReadAdditionalFilesAsync(IEnumerable additionalFiles, Regex fileNamePattern, CancellationToken cancellationToken) + if (fileNamePattern is null) { - if (additionalFiles is null) - { - throw new ArgumentNullException(nameof(additionalFiles)); - } - - if (fileNamePattern is null) - { - throw new ArgumentNullException(nameof(fileNamePattern)); - } - - IEnumerable? docs = from doc in additionalFiles.OrderBy(x => x.FilePath, StringComparer.Ordinal) - let fileName = Path.GetFileName(doc.Name) - where fileNamePattern.IsMatch(fileName) - select doc; - ImmutableArray.Builder? result = ImmutableArray.CreateBuilder(); - foreach (TextDocument? doc in docs) - { - SourceText? text = await doc.GetTextAsync(cancellationToken); - result.AddRange(ReadLinesFromAdditionalFile(text)); - } - - return result.ToImmutable(); + throw new ArgumentNullException(nameof(fileNamePattern)); } + + IEnumerable? docs = from doc in additionalFiles.OrderBy(x => x.FilePath, StringComparer.Ordinal) + let fileName = Path.GetFileName(doc.Name) + where fileNamePattern.IsMatch(fileName) + select doc; + ImmutableArray.Builder? result = ImmutableArray.CreateBuilder(); + foreach (TextDocument? doc in docs) + { + SourceText? text = await doc.GetTextAsync(cancellationToken); + result.AddRange(ReadLinesFromAdditionalFile(text)); + } + + return result.ToImmutable(); } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/FixUtils.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/FixUtils.cs index 97541238c..b408e9ede 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/FixUtils.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/FixUtils.cs @@ -1,443 +1,466 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.Rename; +using Microsoft.CodeAnalysis.Simplification; +using CSSyntax = Microsoft.CodeAnalysis.CSharp.Syntax; +using VB = Microsoft.CodeAnalysis.VisualBasic; +using VBSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +internal static class FixUtils { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CSharp; - using Microsoft.CodeAnalysis.CSharp.Syntax; - using Microsoft.CodeAnalysis.FindSymbols; - using Microsoft.CodeAnalysis.Rename; - using Microsoft.CodeAnalysis.Simplification; - - internal static class FixUtils + internal const string BookmarkAnnotationName = "Bookmark"; + + internal static AnonymousFunctionExpressionSyntax MakeMethodAsync(this AnonymousFunctionExpressionSyntax method, bool hasReturnValue, SemanticModel? semanticModel, CancellationToken cancellationToken) { - internal const string BookmarkAnnotationName = "Bookmark"; + if (method.AsyncKeyword.IsKind(SyntaxKind.AsyncKeyword)) + { + // already async + return method; + } - internal static AnonymousFunctionExpressionSyntax MakeMethodAsync(this AnonymousFunctionExpressionSyntax method, bool hasReturnValue, SemanticModel semanticModel, CancellationToken cancellationToken) + AnonymousFunctionExpressionSyntax? updated = null; + + var simpleLambda = method as SimpleLambdaExpressionSyntax; + if (simpleLambda is object) { - if (method.AsyncKeyword.IsKind(SyntaxKind.AsyncKeyword)) - { - // already async - return method; - } + updated = simpleLambda + .WithAsyncKeyword(SyntaxFactory.Token(SyntaxKind.AsyncKeyword)) + .WithBody(UpdateStatementsForAsyncMethod(simpleLambda.Body, semanticModel, hasReturnValue, cancellationToken)); + } - AnonymousFunctionExpressionSyntax? updated = null; + var parentheticalLambda = method as ParenthesizedLambdaExpressionSyntax; + if (parentheticalLambda is object) + { + updated = parentheticalLambda + .WithAsyncKeyword(SyntaxFactory.Token(SyntaxKind.AsyncKeyword)) + .WithBody(UpdateStatementsForAsyncMethod(parentheticalLambda.Body, semanticModel, hasReturnValue, cancellationToken)); + } - var simpleLambda = method as SimpleLambdaExpressionSyntax; - if (simpleLambda is object) - { - updated = simpleLambda - .WithAsyncKeyword(SyntaxFactory.Token(SyntaxKind.AsyncKeyword)) - .WithBody(UpdateStatementsForAsyncMethod(simpleLambda.Body, semanticModel, hasReturnValue, cancellationToken)); - } + var anonymousMethod = method as AnonymousMethodExpressionSyntax; + if (anonymousMethod is object) + { + updated = anonymousMethod + .WithAsyncKeyword(SyntaxFactory.Token(SyntaxKind.AsyncKeyword)) + .WithBody(UpdateStatementsForAsyncMethod(anonymousMethod.Body, semanticModel, hasReturnValue, cancellationToken)); + } - var parentheticalLambda = method as ParenthesizedLambdaExpressionSyntax; - if (parentheticalLambda is object) - { - updated = parentheticalLambda - .WithAsyncKeyword(SyntaxFactory.Token(SyntaxKind.AsyncKeyword)) - .WithBody(UpdateStatementsForAsyncMethod(parentheticalLambda.Body, semanticModel, hasReturnValue, cancellationToken)); - } + if (updated is null) + { + throw new NotSupportedException(); + } - var anonymousMethod = method as AnonymousMethodExpressionSyntax; - if (anonymousMethod is object) - { - updated = anonymousMethod - .WithAsyncKeyword(SyntaxFactory.Token(SyntaxKind.AsyncKeyword)) - .WithBody(UpdateStatementsForAsyncMethod(anonymousMethod.Body, semanticModel, hasReturnValue, cancellationToken)); - } + return updated; + } - if (updated is null) - { - throw new NotSupportedException(); - } + /// + /// Converts a synchronous method to be asynchronous, if it is not already async. + /// + /// The method to convert. + /// The document. + /// The cancellation token. + /// + /// The new Document and method syntax, or the original if it was already async. + /// + /// + /// If is null. + /// -or- + /// If is null. + /// + internal static async Task> MakeMethodAsync(this MethodDeclarationSyntax method, Document document, CancellationToken cancellationToken = default(CancellationToken)) + { + if (method is null) + { + throw new ArgumentNullException(nameof(method)); + } - return updated; + if (document is null) + { + throw new ArgumentNullException(nameof(document)); } - /// - /// Converts a synchronous method to be asynchronous, if it is not already async. - /// - /// The method to convert. - /// The document. - /// The cancellation token. - /// - /// The new Document and method syntax, or the original if it was already async. - /// - /// - /// If is null. - /// -or- - /// If is null. - /// - internal static async Task> MakeMethodAsync(this MethodDeclarationSyntax method, Document document, CancellationToken cancellationToken = default(CancellationToken)) + if (method.Modifiers.Any(SyntaxKind.AsyncKeyword)) { - if (method is null) - { - throw new ArgumentNullException(nameof(method)); - } + // Already asynchronous. + return Tuple.Create(document, method); + } - if (document is null) - { - throw new ArgumentNullException(nameof(document)); - } + DocumentId documentId = document.Id; + SemanticModel? semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + IMethodSymbol? methodSymbol = semanticModel.GetDeclaredSymbol(method, cancellationToken); - if (method.Modifiers.Any(SyntaxKind.AsyncKeyword)) + bool hasReturnValue; + TypeSyntax returnType = method.ReturnType; + if (!Utils.HasAsyncCompatibleReturnType(methodSymbol)) + { + hasReturnValue = (method.ReturnType as PredefinedTypeSyntax)?.Keyword.IsKind(SyntaxKind.VoidKeyword) is not true; + + // Determine new return type. + returnType = hasReturnValue + ? QualifyName( + Namespaces.SystemThreadingTasks, + SyntaxFactory.GenericName(SyntaxFactory.Identifier(nameof(Task))) + .AddTypeArgumentListArguments(method.ReturnType)) + : SyntaxFactory.ParseTypeName(typeof(Task).FullName); + returnType = returnType + .WithAdditionalAnnotations(Simplifier.Annotation) + .WithTrailingTrivia(method.ReturnType.GetTrailingTrivia()); + } + else + { + TypeSyntax t = method.ReturnType; + while (t is QualifiedNameSyntax q) { - // Already asynchronous. - return Tuple.Create(document, method); + t = q.Right; } - DocumentId documentId = document.Id; - SemanticModel? semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - IMethodSymbol? methodSymbol = semanticModel.GetDeclaredSymbol(method, cancellationToken); + hasReturnValue = t is GenericNameSyntax; + } - bool hasReturnValue; - TypeSyntax returnType = method.ReturnType; - if (!Utils.HasAsyncCompatibleReturnType(methodSymbol)) - { - hasReturnValue = (method.ReturnType as PredefinedTypeSyntax)?.Keyword.IsKind(SyntaxKind.VoidKeyword) is not true; - - // Determine new return type. - returnType = hasReturnValue - ? QualifyName( - Namespaces.SystemThreadingTasks, - SyntaxFactory.GenericName(SyntaxFactory.Identifier(nameof(Task))) - .AddTypeArgumentListArguments(method.ReturnType)) - : SyntaxFactory.ParseTypeName(typeof(Task).FullName); - returnType = returnType - .WithAdditionalAnnotations(Simplifier.Annotation) - .WithTrailingTrivia(method.ReturnType.GetTrailingTrivia()); - } - else - { - TypeSyntax t = method.ReturnType; - while (t is QualifiedNameSyntax q) - { - t = q.Right; - } + // Fix up any return statements to await on the Task it would have returned. + bool returnTypeChanged = method.ReturnType != returnType; + BlockSyntax? updatedBody = method.Body is null ? null : UpdateStatementsForAsyncMethod(method.Body, semanticModel, hasReturnValue, returnTypeChanged, cancellationToken); + + // Apply the changes to the document, and null out stale data. + SyntaxAnnotation methodBookmark; + (document, method, methodBookmark) = await UpdateDocumentAsync( + document, + method, + m => m + .WithBody(updatedBody) + .AddModifiers(SyntaxFactory.Token(SyntaxKind.AsyncKeyword)) + .WithReturnType(returnType), + cancellationToken).ConfigureAwait(false); + semanticModel = null; + methodSymbol = null; + + // Rename the method to have an Async suffix if we changed the return type, + // and it doesn't already have that suffix. + if (returnTypeChanged && !method.Identifier.ValueText.EndsWith(VSTHRD200UseAsyncNamingConventionAnalyzer.MandatoryAsyncSuffix, StringComparison.Ordinal)) + { + string newName = method.Identifier.ValueText + VSTHRD200UseAsyncNamingConventionAnalyzer.MandatoryAsyncSuffix; - hasReturnValue = t is GenericNameSyntax; - } + semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + methodSymbol = semanticModel.GetDeclaredSymbol(method, cancellationToken) ?? throw new InvalidOperationException("Unable to find method symbol."); - // Fix up any return statements to await on the Task it would have returned. - bool returnTypeChanged = method.ReturnType != returnType; - BlockSyntax updatedBody = UpdateStatementsForAsyncMethod( - method.Body, - semanticModel, - hasReturnValue, - returnTypeChanged, - cancellationToken); - - // Apply the changes to the document, and null out stale data. - SyntaxAnnotation methodBookmark; - (document, method, methodBookmark) = await UpdateDocumentAsync( - document, - method, - m => m - .WithBody(updatedBody) - .AddModifiers(SyntaxFactory.Token(SyntaxKind.AsyncKeyword)) - .WithReturnType(returnType), - cancellationToken).ConfigureAwait(false); - semanticModel = null; - methodSymbol = null; - - // Rename the method to have an Async suffix if we changed the return type, - // and it doesn't already have that suffix. - if (returnTypeChanged && !method.Identifier.ValueText.EndsWith(VSTHRD200UseAsyncNamingConventionAnalyzer.MandatoryAsyncSuffix, StringComparison.Ordinal)) + // Don't rename entrypoint (i.e. "Main") methods. + if (!Utils.IsEntrypointMethod(methodSymbol, semanticModel, cancellationToken)) { - string newName = method.Identifier.ValueText + VSTHRD200UseAsyncNamingConventionAnalyzer.MandatoryAsyncSuffix; - - semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - methodSymbol = semanticModel.GetDeclaredSymbol(method, cancellationToken); - - // Don't rename entrypoint (i.e. "Main") methods. - if (!Utils.IsEntrypointMethod(methodSymbol, semanticModel, cancellationToken)) - { - Solution? solution = await Renamer.RenameSymbolAsync( - document.Project.Solution, - methodSymbol, - newName, - document.Project.Solution.Workspace.Options, - cancellationToken).ConfigureAwait(false); - document = solution.GetDocument(document.Id); - semanticModel = null; - methodSymbol = null; - SyntaxNode? root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - method = (MethodDeclarationSyntax)root.GetAnnotatedNodes(methodBookmark).Single(); - } + Solution? solution = await Renamer.RenameSymbolAsync( + document.Project.Solution, + methodSymbol, + default(SymbolRenameOptions), + newName, + cancellationToken).ConfigureAwait(false); + document = solution.GetDocumentOrThrow(document.Id); + semanticModel = null; + methodSymbol = null; + SyntaxNode? root = await document.GetSyntaxRootOrThrowAsync(cancellationToken).ConfigureAwait(false); + method = (MethodDeclarationSyntax)root.GetAnnotatedNodes(methodBookmark).Single(); } + } - // Update callers to await calls to this method if we made it awaitable. - if (returnTypeChanged) + // Update callers to await calls to this method if we made it awaitable. + if (returnTypeChanged) + { + semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + methodSymbol = semanticModel.GetDeclaredSymbol(method, cancellationToken) ?? throw new InvalidOperationException("Unable to find method symbol."); + SyntaxAnnotation callerAnnotation; + Solution solution = document.Project.Solution; + List annotatedDocumentIds; + (solution, callerAnnotation, annotatedDocumentIds) = await AnnotateAllCallersAsync(solution, methodSymbol, cancellationToken).ConfigureAwait(false); + foreach (DocumentId docId in annotatedDocumentIds) { - semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - methodSymbol = semanticModel.GetDeclaredSymbol(method, cancellationToken); - SyntaxAnnotation callerAnnotation; - Solution solution = document.Project.Solution; - List annotatedDocumentIds; - (solution, callerAnnotation, annotatedDocumentIds) = await AnnotateAllCallersAsync(solution, methodSymbol, cancellationToken).ConfigureAwait(false); - foreach (DocumentId docId in annotatedDocumentIds) - { - document = solution.GetDocument(docId); - SyntaxTree? tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - SyntaxNode? root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); - var rewriter = new AwaitCallRewriter(callerAnnotation); - root = rewriter.Visit(root); - solution = solution.GetDocument(tree).WithSyntaxRoot(root).Project.Solution; - } + document = solution.GetDocumentOrThrow(docId); + SyntaxTree tree = await document.GetSyntaxTreeOrThrowAsync(cancellationToken).ConfigureAwait(false); + SyntaxNode root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); + var rewriter = new AwaitCallRewriter(callerAnnotation); + root = rewriter.Visit(root); + solution = solution.GetDocumentOrThrow(tree).WithSyntaxRoot(root).Project.Solution; + } - foreach (DocumentId docId in annotatedDocumentIds) + foreach (DocumentId docId in annotatedDocumentIds) + { + document = solution.GetDocumentOrThrow(docId); + SyntaxTree tree = await document.GetSyntaxTreeOrThrowAsync(cancellationToken).ConfigureAwait(false); + SyntaxNode root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); + for (SyntaxNode? node = root.GetAnnotatedNodes(callerAnnotation).FirstOrDefault(); node is object; node = root.GetAnnotatedNodes(callerAnnotation).FirstOrDefault()) { - document = solution.GetDocument(docId); - SyntaxTree? tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - SyntaxNode? root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); - for (SyntaxNode? node = root.GetAnnotatedNodes(callerAnnotation).FirstOrDefault(); node is object; node = root.GetAnnotatedNodes(callerAnnotation).FirstOrDefault()) + MethodDeclarationSyntax? callingMethod = node.FirstAncestorOrSelf(); + if (callingMethod is object) { - MethodDeclarationSyntax? callingMethod = node.FirstAncestorOrSelf(); - if (callingMethod is object) - { - (document, callingMethod) = await MakeMethodAsync(callingMethod, document, cancellationToken).ConfigureAwait(false); - - // Clear all annotations of callers from this method so we don't revisit it. - root = await callingMethod.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); - var annotationRemover = new RemoveAnnotationRewriter(callerAnnotation); - root = root.ReplaceNode(callingMethod, annotationRemover.Visit(callingMethod)); - document = document.WithSyntaxRoot(root); - root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - } - else - { - // Clear all annotations of callers from this method so we don't revisit it. - root = await node.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); - root = root.ReplaceNode(node, node.WithoutAnnotations(callerAnnotation)); - document = document.WithSyntaxRoot(root); - root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - } + (document, callingMethod) = await MakeMethodAsync(callingMethod, document, cancellationToken).ConfigureAwait(false); + + // Clear all annotations of callers from this method so we don't revisit it. + root = await callingMethod.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + var annotationRemover = new RemoveAnnotationRewriter(callerAnnotation); + root = root.ReplaceNode(callingMethod, annotationRemover.Visit(callingMethod)!); + document = document.WithSyntaxRoot(root); + root = await document.GetSyntaxRootOrThrowAsync(cancellationToken).ConfigureAwait(false); + } + else + { + // Clear all annotations of callers from this method so we don't revisit it. + root = await node.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + root = root.ReplaceNode(node, node.WithoutAnnotations(callerAnnotation)); + document = document.WithSyntaxRoot(root); + root = await document.GetSyntaxRootOrThrowAsync(cancellationToken).ConfigureAwait(false); } - - solution = document.Project.Solution; } - // Make sure we return the latest of everything. - document = solution.GetDocument(documentId); - SyntaxTree? finalTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - SyntaxNode? finalRoot = await finalTree.GetRootAsync(cancellationToken).ConfigureAwait(false); - method = (MethodDeclarationSyntax)finalRoot.GetAnnotatedNodes(methodBookmark).Single(); + solution = document.Project.Solution; } - return Tuple.Create(document, method); + // Make sure we return the latest of everything. + document = solution.GetDocumentOrThrow(documentId); + SyntaxTree finalTree = await document.GetSyntaxTreeOrThrowAsync(cancellationToken).ConfigureAwait(false); + SyntaxNode finalRoot = await finalTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + method = (MethodDeclarationSyntax)finalRoot.GetAnnotatedNodes(methodBookmark).Single(); } - internal static async Task>> AnnotateAllCallersAsync(Solution solution, ISymbol symbol, CancellationToken cancellationToken) + return Tuple.Create(document, method); + } + + internal static async Task>> AnnotateAllCallersAsync(Solution solution, ISymbol symbol, CancellationToken cancellationToken) + { + var bookmark = new SyntaxAnnotation(); + IEnumerable? callers = await SymbolFinder.FindCallersAsync(symbol, solution, cancellationToken).ConfigureAwait(false); + IEnumerable>? callersByFile = from caller in callers + from location in caller.Locations + group location by location.SourceTree into file + select file; + var updatedDocs = new List(); + foreach (IGrouping? callerByFile in callersByFile) { - var bookmark = new SyntaxAnnotation(); - IEnumerable? callers = await SymbolFinder.FindCallersAsync(symbol, solution, cancellationToken).ConfigureAwait(false); - IEnumerable>? callersByFile = from caller in callers - from location in caller.Locations - group location by location.SourceTree into file - select file; - var updatedDocs = new List(); - foreach (IGrouping? callerByFile in callersByFile) + SyntaxNode? root = await callerByFile.Key.GetRootAsync(cancellationToken).ConfigureAwait(false); + foreach (Location? caller in callerByFile) { - SyntaxNode? root = await callerByFile.Key.GetRootAsync(cancellationToken).ConfigureAwait(false); - foreach (Location? caller in callerByFile) + SyntaxNode? node = root.FindNode(caller.SourceSpan); + InvocationExpressionSyntax? invocation = node.FirstAncestorOrSelf(); + if (invocation is object) { - SyntaxNode? node = root.FindNode(caller.SourceSpan); - InvocationExpressionSyntax? invocation = node.FirstAncestorOrSelf(); - if (invocation is object) - { - root = root.ReplaceNode(invocation, invocation.WithAdditionalAnnotations(bookmark)); - } + root = root.ReplaceNode(invocation, invocation.WithAdditionalAnnotations(bookmark)); } - - Document updatedDocument = solution.GetDocument(callerByFile.Key) - .WithSyntaxRoot(root); - updatedDocs.Add(updatedDocument.Id); - solution = updatedDocument.Project.Solution; } - return Tuple.Create(solution, bookmark, updatedDocs); + Document updatedDocument = solution.GetDocumentOrThrow(callerByFile.Key) + .WithSyntaxRoot(root); + updatedDocs.Add(updatedDocument.Id); + solution = updatedDocument.Project.Solution; } - internal static async Task> UpdateDocumentAsync(Document document, T syntaxNode, Func syntaxNodeTransform, CancellationToken cancellationToken) - where T : SyntaxNode + return Tuple.Create(solution, bookmark, updatedDocs); + } + + internal static async Task> UpdateDocumentAsync(Document document, T syntaxNode, Func syntaxNodeTransform, CancellationToken cancellationToken) + where T : SyntaxNode + { + SyntaxAnnotation bookmark; + SyntaxNode root; + (bookmark, document, syntaxNode, root) = await BookmarkSyntaxAsync(document, syntaxNode, cancellationToken).ConfigureAwait(false); + + T? newSyntaxNode = syntaxNodeTransform(syntaxNode); + if (!newSyntaxNode.HasAnnotation(bookmark)) { - SyntaxAnnotation bookmark; - SyntaxNode root; - (bookmark, document, syntaxNode, root) = await BookmarkSyntaxAsync(document, syntaxNode, cancellationToken).ConfigureAwait(false); + newSyntaxNode = syntaxNode.CopyAnnotationsTo(newSyntaxNode)!; + } - T? newSyntaxNode = syntaxNodeTransform(syntaxNode); - if (!newSyntaxNode.HasAnnotation(bookmark)) - { - newSyntaxNode = syntaxNode.CopyAnnotationsTo(newSyntaxNode); - } + root = root.ReplaceNode(syntaxNode, newSyntaxNode); + document = document.WithSyntaxRoot(root); + root = await document.GetSyntaxRootOrThrowAsync(cancellationToken).ConfigureAwait(false); + newSyntaxNode = (T)root.GetAnnotatedNodes(bookmark).Single(); - root = root.ReplaceNode(syntaxNode, newSyntaxNode); - document = document.WithSyntaxRoot(root); - root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - newSyntaxNode = (T)root.GetAnnotatedNodes(bookmark).Single(); + return Tuple.Create(document, newSyntaxNode, bookmark); + } - return Tuple.Create(document, newSyntaxNode, bookmark); + internal static async Task> BookmarkSyntaxAsync(Document document, T syntaxNode, CancellationToken cancellationToken) + where T : SyntaxNode + { + var bookmark = new SyntaxAnnotation(BookmarkAnnotationName); + SyntaxNode root = await syntaxNode.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + root = root.ReplaceNode(syntaxNode, syntaxNode.WithAdditionalAnnotations(bookmark)); + document = document.WithSyntaxRoot(root); + root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false) ?? throw new InvalidOperationException("Unable to find syntax root"); + syntaxNode = (T)root.GetAnnotatedNodes(bookmark).Single(); + + return Tuple.Create(bookmark, document, syntaxNode, root); + } + + internal static NameSyntax QualifyName(IReadOnlyList qualifiers, SimpleNameSyntax simpleName) + { + if (qualifiers is null) + { + throw new ArgumentNullException(nameof(qualifiers)); } - internal static async Task> BookmarkSyntaxAsync(Document document, T syntaxNode, CancellationToken cancellationToken) - where T : SyntaxNode + if (simpleName is null) { - var bookmark = new SyntaxAnnotation(BookmarkAnnotationName); - SyntaxNode? root = await syntaxNode.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); - root = root.ReplaceNode(syntaxNode, syntaxNode.WithAdditionalAnnotations(bookmark)); - document = document.WithSyntaxRoot(root); - root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - syntaxNode = (T)root.GetAnnotatedNodes(bookmark).Single(); - - return Tuple.Create(bookmark, document, syntaxNode, root); + throw new ArgumentNullException(nameof(simpleName)); } - internal static NameSyntax QualifyName(IReadOnlyList qualifiers, SimpleNameSyntax simpleName) + if (qualifiers.Count == 0) { - if (qualifiers is null) - { - throw new ArgumentNullException(nameof(qualifiers)); - } + throw new ArgumentException("At least one qualifier required."); + } - if (simpleName is null) - { - throw new ArgumentNullException(nameof(simpleName)); - } + NameSyntax result = SyntaxFactory.IdentifierName(qualifiers[0]); + for (int i = 1; i < qualifiers.Count; i++) + { + IdentifierNameSyntax? rightSide = SyntaxFactory.IdentifierName(qualifiers[i]); + result = SyntaxFactory.QualifiedName(result, rightSide); + } - if (qualifiers.Count == 0) - { - throw new ArgumentException("At least one qualifier required."); - } + return SyntaxFactory.QualifiedName(result, simpleName); + } - NameSyntax result = SyntaxFactory.IdentifierName(qualifiers[0]); - for (int i = 1; i < qualifiers.Count; i++) - { - IdentifierNameSyntax? rightSide = SyntaxFactory.IdentifierName(qualifiers[i]); - result = SyntaxFactory.QualifiedName(result, rightSide); - } + internal static Document GetDocumentOrThrow(this Solution solution, DocumentId documentId) => solution.GetDocument(documentId) ?? throw new InvalidOperationException("No document by the ID found."); - return SyntaxFactory.QualifiedName(result, simpleName); - } + internal static Document GetDocumentOrThrow(this Solution solution, SyntaxTree syntaxTree) => solution.GetDocument(syntaxTree) ?? throw new InvalidOperationException("No document with the given syntax tree found."); - private static CSharpSyntaxNode UpdateStatementsForAsyncMethod(CSharpSyntaxNode body, SemanticModel semanticModel, bool hasResultValue, CancellationToken cancellationToken) - { - var blockBody = body as BlockSyntax; - if (blockBody is object) - { - bool returnTypeChanged = false; // probably not right, but we don't have a failing test yet. - return UpdateStatementsForAsyncMethod(blockBody, semanticModel, hasResultValue, returnTypeChanged, cancellationToken); - } + internal static async Task GetSyntaxTreeOrThrowAsync(this Document document, CancellationToken cancellationToken) => await document.GetSyntaxTreeAsync(cancellationToken) ?? throw new InvalidOperationException("No syntax tree could be obtained from the document."); - var expressionBody = body as ExpressionSyntax; - if (expressionBody is object) - { - return SyntaxFactory.AwaitExpression(expressionBody).TrySimplify(expressionBody, semanticModel, cancellationToken); - } + internal static async Task GetSyntaxRootOrThrowAsync(this Document document, CancellationToken cancellationToken) => await document.GetSyntaxRootAsync(cancellationToken) ?? throw new InvalidOperationException("No syntax root could be obtained from the document."); - throw new NotSupportedException(); + internal static (SyntaxNode? Creation, SyntaxNode[]? Arguments) FindObjectCreationSyntax(SyntaxNode startFrom) + { + if (startFrom is CSharpSyntaxNode && startFrom.FirstAncestorOrSelf() is { } csCreation) + { + return (csCreation, csCreation.ArgumentList?.Arguments.ToArray()); } - - private static BlockSyntax UpdateStatementsForAsyncMethod(BlockSyntax body, SemanticModel semanticModel, bool hasResultValue, bool returnTypeChanged, CancellationToken cancellationToken) + else if (startFrom is VB.VisualBasicSyntaxNode && startFrom.FirstAncestorOrSelf() is { } vbCreation) { - BlockSyntax? fixedUpBlock = body.ReplaceNodes( - body.DescendantNodes().OfType(), - (f, n) => - { - if (hasResultValue) - { - return returnTypeChanged - ? n - : n.WithExpression(SyntaxFactory.AwaitExpression(n.Expression).TrySimplify(f.Expression, semanticModel, cancellationToken)); - } - - if (body.Statements.Last() == f) - { - // If it is the last statement in the method, we can remove it since a return is implied. - return null; - } - - return n - .WithExpression(null) // don't return any value - .WithReturnKeyword(n.ReturnKeyword.WithTrailingTrivia(SyntaxFactory.TriviaList())); // remove the trailing space after the keyword - }); + return (vbCreation, vbCreation.ArgumentList?.Arguments.ToArray()); + } + else + { + return (null, null); + } + } - return fixedUpBlock; + private static CSharpSyntaxNode UpdateStatementsForAsyncMethod(CSharpSyntaxNode body, SemanticModel? semanticModel, bool hasResultValue, CancellationToken cancellationToken) + { + var blockBody = body as BlockSyntax; + if (blockBody is object) + { + bool returnTypeChanged = false; // probably not right, but we don't have a failing test yet. + return UpdateStatementsForAsyncMethod(blockBody, semanticModel, hasResultValue, returnTypeChanged, cancellationToken); } - private static ExpressionSyntax TrySimplify(this AwaitExpressionSyntax awaitExpression, ExpressionSyntax originalSyntax, SemanticModel semanticModel, CancellationToken cancellationToken) + var expressionBody = body as ExpressionSyntax; + if (expressionBody is object) { - if (awaitExpression is null) - { - throw new ArgumentNullException(nameof(awaitExpression)); - } + return SyntaxFactory.AwaitExpression(expressionBody).TrySimplify(expressionBody, semanticModel, cancellationToken); + } + + throw new NotSupportedException(); + } - // await Task.FromResult(x) => x. - if (semanticModel is object) + private static BlockSyntax UpdateStatementsForAsyncMethod(BlockSyntax body, SemanticModel? semanticModel, bool hasResultValue, bool returnTypeChanged, CancellationToken cancellationToken) + { + BlockSyntax fixedUpBlock = body.ReplaceNodes( + body.DescendantNodes().OfType(), + (f, n) => { - if (awaitExpression.Expression is InvocationExpressionSyntax awaitedInvocation - && awaitedInvocation.Expression is MemberAccessExpressionSyntax awaitedInvocationMemberAccess - && awaitedInvocationMemberAccess.Name.Identifier.Text == nameof(Task.FromResult)) + if (hasResultValue) { - // Is the FromResult method on the Task or Task class? - ISymbol? memberOwnerSymbol = semanticModel.GetSymbolInfo(originalSyntax, cancellationToken).Symbol; - if (Utils.IsTask(memberOwnerSymbol?.ContainingType)) - { - ExpressionSyntax? simplified = awaitedInvocation.ArgumentList.Arguments.Single().Expression; - return simplified; - } + return returnTypeChanged || n.Expression is null || f.Expression is null + ? n + : n.WithExpression(SyntaxFactory.AwaitExpression(n.Expression).TrySimplify(f.Expression, semanticModel, cancellationToken)); } - } - return awaitExpression; - } + if (body.Statements.Last() == f) + { + // If it is the last statement in the method, we can remove it since a return is implied. +#pragma warning disable CS8603 // Possible null reference return. - https://github.com/dotnet/roslyn/issues/65537 + return null; +#pragma warning restore CS8603 // Possible null reference return. + } - private class AwaitCallRewriter : CSharpSyntaxRewriter - { - private readonly SyntaxAnnotation callAnnotation; + return n + .WithExpression(null) // don't return any value + .WithReturnKeyword(n.ReturnKeyword.WithTrailingTrivia(SyntaxFactory.TriviaList())); // remove the trailing space after the keyword + }); - public AwaitCallRewriter(SyntaxAnnotation callAnnotation) - : base(visitIntoStructuredTrivia: false) - { - this.callAnnotation = callAnnotation ?? throw new ArgumentNullException(nameof(callAnnotation)); - } + return fixedUpBlock; + } - public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node) + private static ExpressionSyntax TrySimplify(this AwaitExpressionSyntax awaitExpression, ExpressionSyntax originalSyntax, SemanticModel? semanticModel, CancellationToken cancellationToken) + { + if (awaitExpression is null) + { + throw new ArgumentNullException(nameof(awaitExpression)); + } + + // await Task.FromResult(x) => x. + if (semanticModel is object) + { + if (awaitExpression.Expression is InvocationExpressionSyntax awaitedInvocation + && awaitedInvocation.Expression is MemberAccessExpressionSyntax awaitedInvocationMemberAccess + && awaitedInvocationMemberAccess.Name.Identifier.Text == nameof(Task.FromResult)) { - if (node.HasAnnotation(this.callAnnotation)) + // Is the FromResult method on the Task or Task class? + ISymbol? memberOwnerSymbol = semanticModel.GetSymbolInfo(originalSyntax, cancellationToken).Symbol; + if (Utils.IsTask(memberOwnerSymbol?.ContainingType)) { - return SyntaxFactory.ParenthesizedExpression( - SyntaxFactory.AwaitExpression(node)) - .WithAdditionalAnnotations(Simplifier.Annotation); + ExpressionSyntax? simplified = awaitedInvocation.ArgumentList.Arguments.Single().Expression; + return simplified; } - - return base.VisitInvocationExpression(node); } } - private class RemoveAnnotationRewriter : CSharpSyntaxRewriter + return awaitExpression; + } + + private class AwaitCallRewriter : CSharpSyntaxRewriter + { + private readonly SyntaxAnnotation callAnnotation; + + public AwaitCallRewriter(SyntaxAnnotation callAnnotation) + : base(visitIntoStructuredTrivia: false) { - private readonly SyntaxAnnotation annotationToRemove; + this.callAnnotation = callAnnotation ?? throw new ArgumentNullException(nameof(callAnnotation)); + } - public RemoveAnnotationRewriter(SyntaxAnnotation annotationToRemove) - : base(visitIntoStructuredTrivia: false) + public override SyntaxNode? VisitInvocationExpression(InvocationExpressionSyntax node) + { + if (node.HasAnnotation(this.callAnnotation)) { - this.annotationToRemove = annotationToRemove ?? throw new ArgumentNullException(nameof(annotationToRemove)); + return SyntaxFactory.ParenthesizedExpression( + SyntaxFactory.AwaitExpression(node)) + .WithAdditionalAnnotations(Simplifier.Annotation); } - public override SyntaxNode Visit(SyntaxNode node) - { - return base.Visit( - (node?.HasAnnotation(this.annotationToRemove) ?? false) - ? node.WithoutAnnotations(this.annotationToRemove) - : node); - } + return base.VisitInvocationExpression(node); + } + } + + private class RemoveAnnotationRewriter : CSharpSyntaxRewriter + { + private readonly SyntaxAnnotation annotationToRemove; + + public RemoveAnnotationRewriter(SyntaxAnnotation annotationToRemove) + : base(visitIntoStructuredTrivia: false) + { + this.annotationToRemove = annotationToRemove ?? throw new ArgumentNullException(nameof(annotationToRemove)); + } + + public override SyntaxNode? Visit(SyntaxNode? node) + { + return base.Visit( + (node?.HasAnnotation(this.annotationToRemove) ?? false) + ? node.WithoutAnnotations(this.annotationToRemove) + : node); } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes.csproj b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes.csproj index 3921b92cd..38a21a074 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes.csproj +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes.csproj @@ -1,7 +1,10 @@  + - netstandard1.3 + netstandard2.0 Microsoft.VisualStudio.Threading.Analyzers + true + false Static code analyzer to detect common mistakes or potential issues regarding threading and async coding. true @@ -18,7 +21,7 @@ - + @@ -32,10 +35,8 @@ - - - - + + diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/NullableHelpers.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/NullableHelpers.cs index 77bcc51a1..79c7d256e 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/NullableHelpers.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/NullableHelpers.cs @@ -1,26 +1,25 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using System; +using System; + +namespace Microsoft.VisualStudio.Threading.Analyzers; - internal static class NullableHelpers +internal static class NullableHelpers +{ + /// + /// Converts a delegate which can return to a delegate which does not return + /// . The safety of the conversion is not checked, so callers are required to ensure the + /// conditions are met so the delegate does not produce a result in practice. + /// + /// The type of the first parameter of the method that the delegate encapsulates. + /// The type of the second parameter of the method that the delegate encapsulates. + /// The type of the return value of the method that the delegate encapsulates. + /// The delegate which, according to the signature, can return . + /// A copy of with a signature that does not return . + internal static Func AsNonNullReturnUnchecked(Func func) + where TResult : class { - /// - /// Converts a delegate which can return to a delegate which does not return - /// . The safety of the conversion is not checked, so callers are required to ensure the - /// conditions are met so the delegate does not produce a result in practice. - /// - /// The type of the first parameter of the method that the delegate encapsulates. - /// The type of the second parameter of the method that the delegate encapsulates. - /// The type of the return value of the method that the delegate encapsulates. - /// The delegate which, according to the signature, can return . - /// A copy of with a signature that does not return . - internal static Func AsNonNullReturnUnchecked(Func func) - where TResult : class - { - return func!; - } + return func!; } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/README.md b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/README.md new file mode 100644 index 000000000..0662334ab --- /dev/null +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/README.md @@ -0,0 +1,5 @@ +# Microsoft.VisualStudio.Threading.Analyzers + +Static code analyzers to detect common mistakes or potential issues regarding threading and async coding. + +[Diagnostic analyzer rules](https://microsoft.github.io/vs-threading/analyzers/index.html). diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/SyntaxGeneratorExtensions.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/SyntaxGeneratorExtensions.cs index 7affd363d..d272fc0d8 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/SyntaxGeneratorExtensions.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/SyntaxGeneratorExtensions.cs @@ -1,62 +1,61 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using System.Linq; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Editing; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Editing; + +namespace Microsoft.VisualStudio.Threading.Analyzers; - internal static class SyntaxGeneratorExtensions +internal static class SyntaxGeneratorExtensions +{ + /// + /// Creates a reference to a named type suitable for use in accessing a static member of the type. + /// + /// The used to create the type reference. + /// The named type to reference. + /// A representing the type reference expression. + internal static SyntaxNode TypeExpressionForStaticMemberAccess(this SyntaxGenerator generator, INamedTypeSymbol typeSymbol) { - /// - /// Creates a reference to a named type suitable for use in accessing a static member of the type. - /// - /// The used to create the type reference. - /// The named type to reference. - /// A representing the type reference expression. - internal static SyntaxNode TypeExpressionForStaticMemberAccess(this SyntaxGenerator generator, INamedTypeSymbol typeSymbol) - { - var qualifiedNameSyntaxKind = generator.QualifiedName(generator.IdentifierName("ignored"), generator.IdentifierName("ignored")).RawKind; - var memberAccessExpressionSyntaxKind = generator.MemberAccessExpression(generator.IdentifierName("ignored"), "ignored").RawKind; + var qualifiedNameSyntaxKind = generator.QualifiedName(generator.IdentifierName("ignored"), generator.IdentifierName("ignored")).RawKind; + var memberAccessExpressionSyntaxKind = generator.MemberAccessExpression(generator.IdentifierName("ignored"), "ignored").RawKind; - SyntaxNode? typeExpression = generator.TypeExpression(typeSymbol); - return QualifiedNameToMemberAccess(qualifiedNameSyntaxKind, memberAccessExpressionSyntaxKind, typeExpression, generator); + SyntaxNode? typeExpression = generator.TypeExpression(typeSymbol); + return QualifiedNameToMemberAccess(qualifiedNameSyntaxKind, memberAccessExpressionSyntaxKind, typeExpression, generator); - // Local function - static SyntaxNode QualifiedNameToMemberAccess(int qualifiedNameSyntaxKind, int memberAccessExpressionSyntaxKind, SyntaxNode expression, SyntaxGenerator generator) + // Local function + static SyntaxNode QualifiedNameToMemberAccess(int qualifiedNameSyntaxKind, int memberAccessExpressionSyntaxKind, SyntaxNode expression, SyntaxGenerator generator) + { + if (expression.RawKind == qualifiedNameSyntaxKind) { - if (expression.RawKind == qualifiedNameSyntaxKind) - { - SyntaxNode? left = QualifiedNameToMemberAccess(qualifiedNameSyntaxKind, memberAccessExpressionSyntaxKind, expression.ChildNodes().First(), generator); - SyntaxNode? right = expression.ChildNodes().Last(); - return generator.MemberAccessExpression(left, right); - } - - return expression; + SyntaxNode? left = QualifiedNameToMemberAccess(qualifiedNameSyntaxKind, memberAccessExpressionSyntaxKind, expression.ChildNodes().First(), generator); + SyntaxNode? right = expression.ChildNodes().Last(); + return generator.MemberAccessExpression(left, right); } + + return expression; + } + } + + internal static SyntaxNode? TryGetContainingDeclaration(this SyntaxGenerator generator, SyntaxNode? node, DeclarationKind? kind = null) + { + if (node is null) + { + return null; } - internal static SyntaxNode? TryGetContainingDeclaration(this SyntaxGenerator generator, SyntaxNode? node, DeclarationKind? kind = null) + DeclarationKind declarationKind = generator.GetDeclarationKind(node); + while ((kind.HasValue && declarationKind != kind) || (!kind.HasValue && declarationKind == DeclarationKind.None)) { + node = generator.GetDeclaration(node.Parent); if (node is null) { return null; } - DeclarationKind declarationKind = generator.GetDeclarationKind(node); - while ((kind.HasValue && declarationKind != kind) || (!kind.HasValue && declarationKind == DeclarationKind.None)) - { - node = generator.GetDeclaration(node.Parent); - if (node is null) - { - return null; - } - - declarationKind = generator.GetDeclarationKind(node); - } - - return node; + declarationKind = generator.GetDeclarationKind(node); } + + return node; } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD002UseJtfRunCodeFixWithAwait.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD002UseJtfRunCodeFixWithAwait.cs index bda2eac93..58850f3eb 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD002UseJtfRunCodeFixWithAwait.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD002UseJtfRunCodeFixWithAwait.cs @@ -1,145 +1,146 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Simplification; +using Microsoft.VisualStudio.Threading; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +[ExportCodeFixProvider(LanguageNames.CSharp)] +public class VSTHRD002UseJtfRunCodeFixWithAwait : CodeFixProvider { - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Diagnostics.CodeAnalysis; - using System.Linq; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CodeActions; - using Microsoft.CodeAnalysis.CodeFixes; - using Microsoft.CodeAnalysis.CSharp; - using Microsoft.CodeAnalysis.CSharp.Syntax; - using Microsoft.CodeAnalysis.Simplification; - using Microsoft.VisualStudio.Threading; - - [ExportCodeFixProvider(LanguageNames.CSharp)] - public class VSTHRD002UseJtfRunCodeFixWithAwait : CodeFixProvider - { - private static readonly ImmutableArray ReusableFixableDiagnosticIds = ImmutableArray.Create( - VSTHRD002UseJtfRunAnalyzer.Id); + private static readonly ImmutableArray ReusableFixableDiagnosticIds = ImmutableArray.Create( + VSTHRD002UseJtfRunAnalyzer.Id); - public override ImmutableArray FixableDiagnosticIds => ReusableFixableDiagnosticIds; + public override ImmutableArray FixableDiagnosticIds => ReusableFixableDiagnosticIds; - public override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - Diagnostic? diagnostic = context.Diagnostics.First(); + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + Diagnostic? diagnostic = context.Diagnostics.First(); - SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + SyntaxNode root = await context.Document.GetSyntaxRootOrThrowAsync(context.CancellationToken).ConfigureAwait(false); - if (TryFindNodeAtSource(diagnostic, root, out _, out _)) - { - context.RegisterCodeFix( - CodeAction.Create( - Strings.VSTHRD002_CodeFix_Await_Title, - async ct => + if (TryFindNodeAtSource(diagnostic, root, out _, out _)) + { + context.RegisterCodeFix( + CodeAction.Create( + Strings.VSTHRD002_CodeFix_Await_Title, + async ct => + { + Document? document = context.Document; + if (TryFindNodeAtSource(diagnostic, root, out ExpressionSyntax? node, out Func? transform)) { - Document? document = context.Document; - if (TryFindNodeAtSource(diagnostic, root, out ExpressionSyntax? node, out Func? transform)) + (document, node, _) = await FixUtils.UpdateDocumentAsync( + document, + node, + n => SyntaxFactory.AwaitExpression(transform(n, ct)), + ct).ConfigureAwait(false); + MethodDeclarationSyntax? method = node.FirstAncestorOrSelf(); + if (method is object) { - (document, node, _) = await FixUtils.UpdateDocumentAsync( - document, - node, - n => SyntaxFactory.AwaitExpression(transform(n, ct)), - ct).ConfigureAwait(false); - MethodDeclarationSyntax? method = node.FirstAncestorOrSelf(); - if (method is object) - { - (document, method) = await FixUtils.MakeMethodAsync(method, document, ct).ConfigureAwait(false); - } + (document, method) = await FixUtils.MakeMethodAsync(method, document, ct).ConfigureAwait(false); } + } - return document.Project.Solution; - }, - "only action"), - diagnostic); - } + return document.Project.Solution; + }, + "only action"), + diagnostic); } + } + + /// + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; - /// - public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + private static bool TryFindNodeAtSource(Diagnostic diagnostic, SyntaxNode root, [NotNullWhen(true)] out ExpressionSyntax? target, [NotNullWhen(true)] out Func? transform) + { + transform = null; + target = null; - private static bool TryFindNodeAtSource(Diagnostic diagnostic, SyntaxNode root, [NotNullWhen(true)] out ExpressionSyntax? target, [NotNullWhen(true)] out Func? transform) + var syntaxNode = (ExpressionSyntax)root.FindNode(diagnostic.Location.SourceSpan); + if (syntaxNode.FirstAncestorOrSelf() is object) { - transform = null; - target = null; + // We don't support converting anonymous delegates to async. + return false; + } - var syntaxNode = (ExpressionSyntax)root.FindNode(diagnostic.Location.SourceSpan); - if (syntaxNode.FirstAncestorOrSelf() is object) + SimpleNameSyntax? FindStaticWaitInvocation(ExpressionSyntax? from) + { + SimpleNameSyntax? name = ((from as InvocationExpressionSyntax)?.Expression as MemberAccessExpressionSyntax)?.Name; + return name?.Identifier.ValueText switch { - // We don't support converting anonymous delegates to async. - return false; - } + nameof(Task.WaitAny) => name, + nameof(Task.WaitAll) => name, + _ => null, + }; + } - SimpleNameSyntax? FindStaticWaitInvocation(ExpressionSyntax from) - { - SimpleNameSyntax? name = ((from as InvocationExpressionSyntax)?.Expression as MemberAccessExpressionSyntax)?.Name; - return name?.Identifier.ValueText switch - { - nameof(Task.WaitAny) => name, - nameof(Task.WaitAll) => name, - _ => null, - }; - } - - ExpressionSyntax? TransformStaticWhatInvocation(ExpressionSyntax from, CancellationToken cancellationToken = default(CancellationToken)) - { - SimpleNameSyntax? name = FindStaticWaitInvocation(from); - var newIdentifier = name!.Identifier.ValueText switch - { - nameof(Task.WaitAny) => nameof(Task.WhenAny), - nameof(Task.WaitAll) => nameof(Task.WhenAll), - _ => throw new InvalidOperationException(), - }; - - return from.ReplaceToken(name.Identifier, SyntaxFactory.Identifier(newIdentifier)).WithoutAnnotations(FixUtils.BookmarkAnnotationName); - } - - ExpressionSyntax? FindTwoLevelDeepIdentifierInvocation(ExpressionSyntax from, CancellationToken cancellationToken = default(CancellationToken)) => - ((((from as InvocationExpressionSyntax)?.Expression as MemberAccessExpressionSyntax)?.Expression as InvocationExpressionSyntax)?.Expression as MemberAccessExpressionSyntax)?.Expression; - ExpressionSyntax? FindOneLevelDeepIdentifierInvocation(ExpressionSyntax from, CancellationToken cancellationToken = default(CancellationToken)) => - ((from as InvocationExpressionSyntax)?.Expression as MemberAccessExpressionSyntax)?.Expression; - ExpressionSyntax? FindParentMemberAccess(ExpressionSyntax from, CancellationToken cancellationToken = default(CancellationToken)) => - (from as MemberAccessExpressionSyntax)?.Expression; - - InvocationExpressionSyntax? parentInvocation = syntaxNode.FirstAncestorOrSelf(); - MemberAccessExpressionSyntax? parentMemberAccess = syntaxNode.FirstAncestorOrSelf(); - if (FindTwoLevelDeepIdentifierInvocation(parentInvocation) is object) - { - // This method will not return null for the provided 'target' argument - transform = NullableHelpers.AsNonNullReturnUnchecked(FindTwoLevelDeepIdentifierInvocation); - target = parentInvocation; - } - else if (FindStaticWaitInvocation(parentInvocation) is object) - { - // This method will not return null for the provided 'target' argument - transform = NullableHelpers.AsNonNullReturnUnchecked(TransformStaticWhatInvocation); - target = parentInvocation; - } - else if (FindOneLevelDeepIdentifierInvocation(parentInvocation) is object) - { - // This method will not return null for the provided 'target' argument - transform = NullableHelpers.AsNonNullReturnUnchecked(FindOneLevelDeepIdentifierInvocation); - target = parentInvocation; - } - else if (FindParentMemberAccess(parentMemberAccess) is object) - { - // This method will not return null for the provided 'target' argument - transform = NullableHelpers.AsNonNullReturnUnchecked(FindParentMemberAccess); - target = parentMemberAccess; - } - else + ExpressionSyntax? TransformStaticWhatInvocation(ExpressionSyntax from, CancellationToken cancellationToken = default(CancellationToken)) + { + SimpleNameSyntax? name = FindStaticWaitInvocation(from); + var newIdentifier = name!.Identifier.ValueText switch { - return false; - } + nameof(Task.WaitAny) => nameof(Task.WhenAny), + nameof(Task.WaitAll) => nameof(Task.WhenAll), + _ => throw new InvalidOperationException(), + }; + + return from.ReplaceToken(name.Identifier, SyntaxFactory.Identifier(newIdentifier)).WithoutAnnotations(FixUtils.BookmarkAnnotationName); + } + + ExpressionSyntax? FindTwoLevelDeepIdentifierInvocation(ExpressionSyntax? from, CancellationToken cancellationToken = default(CancellationToken)) => + ((((from as InvocationExpressionSyntax)?.Expression as MemberAccessExpressionSyntax)?.Expression as InvocationExpressionSyntax)?.Expression as MemberAccessExpressionSyntax)?.Expression; + ExpressionSyntax? FindOneLevelDeepIdentifierInvocation(ExpressionSyntax? from, CancellationToken cancellationToken = default(CancellationToken)) => + ((from as InvocationExpressionSyntax)?.Expression as MemberAccessExpressionSyntax)?.Expression; + ExpressionSyntax? FindParentMemberAccess(ExpressionSyntax? from, CancellationToken cancellationToken = default(CancellationToken)) => + (from as MemberAccessExpressionSyntax)?.Expression; + InvocationExpressionSyntax? parentInvocation = syntaxNode.FirstAncestorOrSelf(); + MemberAccessExpressionSyntax? parentMemberAccess = syntaxNode.FirstAncestorOrSelf(); + if (FindTwoLevelDeepIdentifierInvocation(parentInvocation) is object) + { + // This method will not return null for the provided 'target' argument + transform = NullableHelpers.AsNonNullReturnUnchecked(FindTwoLevelDeepIdentifierInvocation); + target = parentInvocation!; + return true; + } + else if (FindStaticWaitInvocation(parentInvocation) is object) + { + // This method will not return null for the provided 'target' argument + transform = NullableHelpers.AsNonNullReturnUnchecked(TransformStaticWhatInvocation); + target = parentInvocation!; return true; } + else if (FindOneLevelDeepIdentifierInvocation(parentInvocation) is object) + { + // This method will not return null for the provided 'target' argument + transform = NullableHelpers.AsNonNullReturnUnchecked(FindOneLevelDeepIdentifierInvocation); + target = parentInvocation!; + return true; + } + else if (FindParentMemberAccess(parentMemberAccess) is object) + { + // This method will not return null for the provided 'target' argument + transform = NullableHelpers.AsNonNullReturnUnchecked(FindParentMemberAccess); + target = parentMemberAccess!; + return true; + } + else + { + return false; + } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD010MainThreadUsageCodeFix.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD010MainThreadUsageCodeFix.cs index dbddb3e14..c1f5136ad 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD010MainThreadUsageCodeFix.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD010MainThreadUsageCodeFix.cs @@ -1,147 +1,146 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Simplification; +using Microsoft.VisualStudio.Threading; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +[ExportCodeFixProvider(LanguageNames.CSharp)] +public class VSTHRD010MainThreadUsageCodeFix : CodeFixProvider { - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Linq; - using System.Text; - using System.Text.RegularExpressions; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CodeActions; - using Microsoft.CodeAnalysis.CodeFixes; - using Microsoft.CodeAnalysis.CSharp; - using Microsoft.CodeAnalysis.CSharp.Syntax; - using Microsoft.CodeAnalysis.Formatting; - using Microsoft.CodeAnalysis.Simplification; - using Microsoft.VisualStudio.Threading; - - [ExportCodeFixProvider(LanguageNames.CSharp)] - public class VSTHRD010MainThreadUsageCodeFix : CodeFixProvider - { - private static readonly ImmutableArray ReusableFixableDiagnosticIds = ImmutableArray.Create( - VSTHRD010MainThreadUsageAnalyzer.Id); + private static readonly ImmutableArray ReusableFixableDiagnosticIds = ImmutableArray.Create( + VSTHRD010MainThreadUsageAnalyzer.Id); - public override ImmutableArray FixableDiagnosticIds => ReusableFixableDiagnosticIds; + public override ImmutableArray FixableDiagnosticIds => ReusableFixableDiagnosticIds; - /// - public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + /// + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; - public override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - Diagnostic? diagnostic = context.Diagnostics.First(); + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + Diagnostic? diagnostic = context.Diagnostics.First(); - SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - var syntaxNode = (ExpressionSyntax)root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + SyntaxNode root = await context.Document.GetSyntaxRootOrThrowAsync(context.CancellationToken).ConfigureAwait(false); + var syntaxNode = (ExpressionSyntax?)root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); - CSharpUtils.ContainingFunctionData container = CSharpUtils.GetContainingFunction(syntaxNode); - if (container.BlockOrExpression is null) - { - return; - } + CSharpUtils.ContainingFunctionData container = CSharpUtils.GetContainingFunction(syntaxNode); + if (container.BlockOrExpression is null) + { + return; + } - SemanticModel? semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); - ISymbol? enclosingSymbol = semanticModel.GetEnclosingSymbol(diagnostic.Location.SourceSpan.Start, context.CancellationToken); - if (enclosingSymbol is null) - { - return; - } + SemanticModel? semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + ISymbol? enclosingSymbol = semanticModel?.GetEnclosingSymbol(diagnostic.Location.SourceSpan.Start, context.CancellationToken); + if (enclosingSymbol is null) + { + return; + } - bool convertToAsync = !container.IsAsync && Utils.HasAsyncCompatibleReturnType(enclosingSymbol as IMethodSymbol); - if (convertToAsync) - { - // We don't support this yet, and we don't want to take the sync method path in this case. - // The user will have to fix this themselves. - return; - } + bool convertToAsync = !container.IsAsync && Utils.HasAsyncCompatibleReturnType(enclosingSymbol as IMethodSymbol); + if (convertToAsync) + { + // We don't support this yet, and we don't want to take the sync method path in this case. + // The user will have to fix this themselves. + return; + } - Regex lookupKey = (container.IsAsync || convertToAsync) - ? CommonInterest.FileNamePatternForMethodsThatSwitchToMainThread - : CommonInterest.FileNamePatternForMethodsThatAssertMainThread; - string[] options = diagnostic.Properties[lookupKey.ToString()].Split('\n'); - if (options.Length > 0) - { - // For any symbol lookups, we want to consider the position of the very first statement in the block. - int positionForLookup = container.BlockOrExpression.GetLocation().SourceSpan.Start + 1; + Regex lookupKey = (container.IsAsync || convertToAsync) + ? CommonInterest.FileNamePatternForMethodsThatSwitchToMainThread + : CommonInterest.FileNamePatternForMethodsThatAssertMainThread; + string[]? options = diagnostic.Properties[lookupKey.ToString()]?.Split('\n'); + if (options?.Length > 0) + { + // For any symbol lookups, we want to consider the position of the very first statement in the block. + int positionForLookup = container.BlockOrExpression.GetLocation().SourceSpan.Start + 1; - Lazy cancellationTokenSymbol = new Lazy(() => Utils.FindCancellationToken(semanticModel, positionForLookup, context.CancellationToken).FirstOrDefault()); - foreach (var option in options) + Lazy cancellationTokenSymbol = new Lazy(() => Utils.FindCancellationToken(semanticModel, positionForLookup, context.CancellationToken).FirstOrDefault()); + foreach (var option in options) + { + // We're looking for methods that either require no parameters, + // or (if we have one to give) that have just one parameter that is a CancellationToken. + IMethodSymbol? proposedMethod = Utils.FindMethodGroup(semanticModel, option) + .FirstOrDefault(m => !m.Parameters.Any(p => !p.HasExplicitDefaultValue) || + (cancellationTokenSymbol.Value is object && m.Parameters.Length == 1 && Utils.IsCancellationTokenParameter(m.Parameters[0]))); + if (proposedMethod is null) { - // We're looking for methods that either require no parameters, - // or (if we have one to give) that have just one parameter that is a CancellationToken. - IMethodSymbol? proposedMethod = Utils.FindMethodGroup(semanticModel, option) - .FirstOrDefault(m => !m.Parameters.Any(p => !p.HasExplicitDefaultValue) || - (cancellationTokenSymbol.Value is object && m.Parameters.Length == 1 && Utils.IsCancellationTokenParameter(m.Parameters[0]))); - if (proposedMethod is null) - { - // We can't find it, so don't offer to use it. - continue; - } + // We can't find it, so don't offer to use it. + continue; + } - if (proposedMethod.IsStatic) - { - OfferFix(option); - } - else + if (proposedMethod.IsStatic) + { + OfferFix(option); + } + else if (semanticModel is not null) + { + foreach (Tuple? candidate in Utils.FindInstanceOf(proposedMethod.ContainingType, semanticModel, positionForLookup, context.CancellationToken)) { - foreach (Tuple? candidate in Utils.FindInstanceOf(proposedMethod.ContainingType, semanticModel, positionForLookup, context.CancellationToken)) + if (candidate.Item1) { - if (candidate.Item1) - { - OfferFix($"{candidate.Item2.Name}.{proposedMethod.Name}"); - } - else - { - OfferFix($"{candidate.Item2.ContainingNamespace}.{candidate.Item2.ContainingType.Name}.{candidate.Item2.Name}.{proposedMethod.Name}"); - } + OfferFix($"{candidate.Item2.Name}.{proposedMethod.Name}"); + } + else + { + OfferFix($"{candidate.Item2.ContainingNamespace}.{candidate.Item2.ContainingType.Name}.{candidate.Item2.Name}.{proposedMethod.Name}"); } } + } - void OfferFix(string fullyQualifiedMethod) - { - context.RegisterCodeFix(CodeAction.Create($"Add call to {fullyQualifiedMethod}", ct => Fix(fullyQualifiedMethod, proposedMethod, cancellationTokenSymbol), fullyQualifiedMethod), context.Diagnostics); - } + void OfferFix(string fullyQualifiedMethod) + { + context.RegisterCodeFix(CodeAction.Create($"Add call to {fullyQualifiedMethod}", ct => Fix(fullyQualifiedMethod, proposedMethod, cancellationTokenSymbol), fullyQualifiedMethod), context.Diagnostics); } } + } - Task Fix(string fullyQualifiedMethod, IMethodSymbol methodSymbol, Lazy cancellationTokenSymbol) + Task Fix(string fullyQualifiedMethod, IMethodSymbol methodSymbol, Lazy cancellationTokenSymbol) + { + int typeAndMethodDelimiterIndex = fullyQualifiedMethod.LastIndexOf('.'); + IdentifierNameSyntax methodName = SyntaxFactory.IdentifierName(fullyQualifiedMethod.Substring(typeAndMethodDelimiterIndex + 1)); + ExpressionSyntax invokedMethod = CSharpUtils.MemberAccess(fullyQualifiedMethod.Substring(0, typeAndMethodDelimiterIndex).Split('.'), methodName); + InvocationExpressionSyntax? invocationExpression = SyntaxFactory.InvocationExpression(invokedMethod); + IParameterSymbol? cancellationTokenParameter = methodSymbol.Parameters.FirstOrDefault(Utils.IsCancellationTokenParameter); + if (cancellationTokenParameter is object && cancellationTokenSymbol.Value is object) { - int typeAndMethodDelimiterIndex = fullyQualifiedMethod.LastIndexOf('.'); - IdentifierNameSyntax methodName = SyntaxFactory.IdentifierName(fullyQualifiedMethod.Substring(typeAndMethodDelimiterIndex + 1)); - ExpressionSyntax invokedMethod = CSharpUtils.MemberAccess(fullyQualifiedMethod.Substring(0, typeAndMethodDelimiterIndex).Split('.'), methodName); - InvocationExpressionSyntax? invocationExpression = SyntaxFactory.InvocationExpression(invokedMethod); - IParameterSymbol? cancellationTokenParameter = methodSymbol.Parameters.FirstOrDefault(Utils.IsCancellationTokenParameter); - if (cancellationTokenParameter is object && cancellationTokenSymbol.Value is object) + ArgumentSyntax? arg = SyntaxFactory.Argument(SyntaxFactory.IdentifierName(cancellationTokenSymbol.Value.Name)); + if (methodSymbol.Parameters.IndexOf(cancellationTokenParameter) > 0) { - ArgumentSyntax? arg = SyntaxFactory.Argument(SyntaxFactory.IdentifierName(cancellationTokenSymbol.Value.Name)); - if (methodSymbol.Parameters.IndexOf(cancellationTokenParameter) > 0) - { - arg = arg.WithNameColon(SyntaxFactory.NameColon(SyntaxFactory.IdentifierName(cancellationTokenParameter.Name))); - } - - invocationExpression = invocationExpression.AddArgumentListArguments(arg); + arg = arg.WithNameColon(SyntaxFactory.NameColon(SyntaxFactory.IdentifierName(cancellationTokenParameter.Name))); } - ExpressionSyntax? awaitExpression = container.IsAsync ? SyntaxFactory.AwaitExpression(invocationExpression) : null; - ExpressionStatementSyntax? addedStatement = SyntaxFactory.ExpressionStatement(awaitExpression ?? invocationExpression) - .WithAdditionalAnnotations(Simplifier.Annotation, Formatter.Annotation); - var initialBlockSyntax = container.BlockOrExpression as BlockSyntax; - if (initialBlockSyntax is null) - { - SyntaxToken openBrace = SyntaxFactory.Token(SyntaxFactory.TriviaList(), SyntaxKind.OpenBraceToken, SyntaxFactory.TriviaList(SyntaxFactory.EndOfLine("\r\n"))); - SyntaxToken closeBrace = SyntaxFactory.Token(SyntaxKind.CloseBraceToken); - SyntaxList statementList = SyntaxFactory.List(new[] { SyntaxFactory.ReturnStatement((ExpressionSyntax)container.BlockOrExpression) }); - initialBlockSyntax = SyntaxFactory.Block(openBrace, statementList, closeBrace) - .WithAdditionalAnnotations(Formatter.Annotation); - } + invocationExpression = invocationExpression.AddArgumentListArguments(arg); + } - BlockSyntax? newBlock = initialBlockSyntax.WithStatements(initialBlockSyntax.Statements.Insert(0, addedStatement)); - return Task.FromResult(context.Document.WithSyntaxRoot(root.ReplaceNode(container.BlockOrExpression.Parent, container.BodyReplacement(newBlock)))); + ExpressionSyntax? awaitExpression = container.IsAsync ? SyntaxFactory.AwaitExpression(invocationExpression) : null; + ExpressionStatementSyntax? addedStatement = SyntaxFactory.ExpressionStatement(awaitExpression ?? invocationExpression) + .WithAdditionalAnnotations(Simplifier.Annotation, Formatter.Annotation); + var initialBlockSyntax = container.BlockOrExpression as BlockSyntax; + if (initialBlockSyntax is null) + { + SyntaxToken openBrace = SyntaxFactory.Token(SyntaxFactory.TriviaList(), SyntaxKind.OpenBraceToken, SyntaxFactory.TriviaList(SyntaxFactory.EndOfLine("\r\n"))); + SyntaxToken closeBrace = SyntaxFactory.Token(SyntaxKind.CloseBraceToken); + SyntaxList statementList = SyntaxFactory.List(new[] { SyntaxFactory.ReturnStatement((ExpressionSyntax)container.BlockOrExpression) }); + initialBlockSyntax = SyntaxFactory.Block(openBrace, statementList, closeBrace) + .WithAdditionalAnnotations(Formatter.Annotation); } + + BlockSyntax? newBlock = initialBlockSyntax.WithStatements(initialBlockSyntax.Statements.Insert(0, addedStatement)); + return Task.FromResult(context.Document.WithSyntaxRoot(root.ReplaceNode(container.BlockOrExpression.Parent!, container.BodyReplacement(newBlock))!)); } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD100AsyncVoidMethodCodeFix.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD100AsyncVoidMethodCodeFix.cs index 07b04f8a7..5bcc32779 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD100AsyncVoidMethodCodeFix.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD100AsyncVoidMethodCodeFix.cs @@ -1,91 +1,90 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Simplification; +using Microsoft.VisualStudio.Threading; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +/// +/// Provides a code action to fix the Async Void Method by changing the return type to Task. +/// +/// +/// [Background] Async void methods have different error-handling semantics. +/// When an exception is thrown out of an async Task or async method/lambda, +/// that exception is captured and placed on the Task object. With async void methods, +/// there is no Task object, so any exceptions thrown out of an async void method will +/// be raised directly on the SynchronizationContext that was active when the async +/// void method started, and it would crash the process. +/// Refer to Stephen's article https://msdn.microsoft.com/en-us/magazine/jj991977.aspx for more info. +/// +/// i.e. +/// +/// +[ExportCodeFixProvider(LanguageNames.CSharp)] +public class VSTHRD100AsyncVoidMethodCodeFix : CodeFixProvider { - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Linq; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CodeActions; - using Microsoft.CodeAnalysis.CodeFixes; - using Microsoft.CodeAnalysis.CSharp; - using Microsoft.CodeAnalysis.CSharp.Syntax; - using Microsoft.CodeAnalysis.Simplification; - using Microsoft.VisualStudio.Threading; + private static readonly ImmutableArray ReusableFixableDiagnosticIds = ImmutableArray.Create( + VSTHRD100AsyncVoidMethodAnalyzer.Id); - /// - /// Provides a code action to fix the Async Void Method by changing the return type to Task. - /// - /// - /// [Background] Async void methods have different error-handling semantics. - /// When an exception is thrown out of an async Task or async method/lambda, - /// that exception is captured and placed on the Task object. With async void methods, - /// there is no Task object, so any exceptions thrown out of an async void method will - /// be raised directly on the SynchronizationContext that was active when the async - /// void method started, and it would crash the process. - /// Refer to Stephen's article https://msdn.microsoft.com/en-us/magazine/jj991977.aspx for more info. - /// - /// i.e. - /// - /// - [ExportCodeFixProvider(LanguageNames.CSharp)] - public class VSTHRD100AsyncVoidMethodCodeFix : CodeFixProvider + /// + public override ImmutableArray FixableDiagnosticIds => ReusableFixableDiagnosticIds; + + /// + public override Task RegisterCodeFixesAsync(CodeFixContext context) { - private static readonly ImmutableArray ReusableFixableDiagnosticIds = ImmutableArray.Create( - VSTHRD100AsyncVoidMethodAnalyzer.Descriptor.Id); + Diagnostic? diagnostic = context.Diagnostics.First(); + context.RegisterCodeFix(new VoidToTaskCodeAction(context.Document, diagnostic), diagnostic); + return Task.FromResult(null); + } - /// - public override ImmutableArray FixableDiagnosticIds => ReusableFixableDiagnosticIds; + /// + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; - /// - public override Task RegisterCodeFixesAsync(CodeFixContext context) + private class VoidToTaskCodeAction : CodeAction + { + private Document document; + private Diagnostic diagnostic; + + internal VoidToTaskCodeAction(Document document, Diagnostic diagnostic) { - Diagnostic? diagnostic = context.Diagnostics.First(); - context.RegisterCodeFix(new VoidToTaskCodeAction(context.Document, diagnostic), diagnostic); - return Task.FromResult(null); + this.document = document; + this.diagnostic = diagnostic; } /// - public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; - - private class VoidToTaskCodeAction : CodeAction - { - private Document document; - private Diagnostic diagnostic; - - internal VoidToTaskCodeAction(Document document, Diagnostic diagnostic) - { - this.document = document; - this.diagnostic = diagnostic; - } + public override string Title => Strings.VSTHRD100_CodeFix_Title; - /// - public override string Title => Strings.VSTHRD100_CodeFix_Title; - - /// - public override string? EquivalenceKey => null; + /// + public override string? EquivalenceKey => null; - protected override async Task GetChangedDocumentAsync(CancellationToken cancellationToken) - { - SyntaxNode? root = await this.document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - MethodDeclarationSyntax? methodDeclaration = root.FindNode(this.diagnostic.Location.SourceSpan).FirstAncestorOrSelf(); - TypeSyntax? taskType = SyntaxFactory.ParseTypeName(typeof(Task).FullName) - .WithAdditionalAnnotations(Simplifier.Annotation) - .WithTrailingTrivia(methodDeclaration.ReturnType.GetTrailingTrivia()); - MethodDeclarationSyntax? newMethodDeclaration = methodDeclaration.WithReturnType(taskType); - SyntaxNode? newRoot = root.ReplaceNode(methodDeclaration, newMethodDeclaration); - Document? newDocument = this.document.WithSyntaxRoot(newRoot); - return newDocument; - } + protected override async Task GetChangedDocumentAsync(CancellationToken cancellationToken) + { + SyntaxNode? root = await this.document.GetSyntaxRootOrThrowAsync(cancellationToken).ConfigureAwait(false); + MethodDeclarationSyntax methodDeclaration = root.FindNode(this.diagnostic.Location.SourceSpan).FirstAncestorOrSelf() ?? throw new InvalidOperationException("Unable to find MethodDeclaration"); + TypeSyntax? taskType = SyntaxFactory.ParseTypeName(typeof(Task).FullName) + .WithAdditionalAnnotations(Simplifier.Annotation) + .WithTrailingTrivia(methodDeclaration.ReturnType.GetTrailingTrivia()); + MethodDeclarationSyntax? newMethodDeclaration = methodDeclaration.WithReturnType(taskType); + SyntaxNode? newRoot = root.ReplaceNode(methodDeclaration, newMethodDeclaration); + Document? newDocument = this.document.WithSyntaxRoot(newRoot); + return newDocument; } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD103UseAsyncOptionCodeFix.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD103UseAsyncOptionCodeFix.cs index 8369b4e0e..682cc9374 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD103UseAsyncOptionCodeFix.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD103UseAsyncOptionCodeFix.cs @@ -1,233 +1,241 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Simplification; +using Microsoft.VisualStudio.Threading; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +/// +/// Provides a code action to fix calls to synchronous methods from async methods when async options exist. +/// +/// +/// +/// +[ExportCodeFixProvider(LanguageNames.CSharp)] +public class VSTHRD103UseAsyncOptionCodeFix : CodeFixProvider { - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Globalization; - using System.Linq; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CodeActions; - using Microsoft.CodeAnalysis.CodeFixes; - using Microsoft.CodeAnalysis.CSharp; - using Microsoft.CodeAnalysis.CSharp.Syntax; - using Microsoft.CodeAnalysis.Simplification; - using Microsoft.VisualStudio.Threading; - - /// - /// Provides a code action to fix calls to synchronous methods from async methods when async options exist. - /// - /// - /// - /// - [ExportCodeFixProvider(LanguageNames.CSharp)] - public class VSTHRD103UseAsyncOptionCodeFix : CodeFixProvider - { - private static readonly ImmutableArray ReusableFixableDiagnosticIds = ImmutableArray.Create( - VSTHRD103UseAsyncOptionAnalyzer.Id); + private static readonly ImmutableArray ReusableFixableDiagnosticIds = ImmutableArray.Create( + VSTHRD103UseAsyncOptionAnalyzer.Id); - /// - public override ImmutableArray FixableDiagnosticIds => ReusableFixableDiagnosticIds; + /// + public override ImmutableArray FixableDiagnosticIds => ReusableFixableDiagnosticIds; - /// - public override async Task RegisterCodeFixesAsync(CodeFixContext context) + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + Diagnostic? diagnostic = context.Diagnostics.FirstOrDefault(d => d.Properties.ContainsKey(VSTHRD103UseAsyncOptionAnalyzer.AsyncMethodKeyName)); + if (diagnostic is object) { - Diagnostic? diagnostic = context.Diagnostics.FirstOrDefault(d => d.Properties.ContainsKey(VSTHRD103UseAsyncOptionAnalyzer.AsyncMethodKeyName)); - if (diagnostic is object) + // Check that the method we're replacing the sync blocking call with actually exists. + // This is particularly useful when the method is an extension method, since the using directive + // would need to be present (or the namespace imply it) and we don't yet add missing using directives. + bool asyncAlternativeExists = false; + string? asyncMethodName = diagnostic.Properties[VSTHRD103UseAsyncOptionAnalyzer.AsyncMethodKeyName]; + if (string.IsNullOrEmpty(asyncMethodName)) { - // Check that the method we're replacing the sync blocking call with actually exists. - // This is particularly useful when the method is an extension method, since the using directive - // would need to be present (or the namespace imply it) and we don't yet add missing using directives. - bool asyncAlternativeExists = false; - string asyncMethodName = diagnostic.Properties[VSTHRD103UseAsyncOptionAnalyzer.AsyncMethodKeyName]; - if (string.IsNullOrEmpty(asyncMethodName)) - { - asyncMethodName = "GetAwaiter"; - } + asyncMethodName = "GetAwaiter"; + } - SemanticModel? semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); - SyntaxNode? syntaxRoot = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - var blockingIdentifier = syntaxRoot.FindNode(diagnostic.Location.SourceSpan) as IdentifierNameSyntax; - var memberAccessExpression = blockingIdentifier?.Parent as MemberAccessExpressionSyntax; + SemanticModel? semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + SyntaxNode syntaxRoot = await context.Document.GetSyntaxRootOrThrowAsync(context.CancellationToken).ConfigureAwait(false); + var blockingIdentifier = syntaxRoot.FindNode(diagnostic.Location.SourceSpan) as IdentifierNameSyntax; + if (blockingIdentifier?.Parent is MemberBindingExpressionSyntax) // ?. conditional access expressions. + { + // Fixes for these are complex, and the violations rare. So we won't automate a code fix for them. + return; + } - // Check whether this code was already calling the awaiter (in a synchronous fashion). - asyncAlternativeExists |= memberAccessExpression?.Expression is InvocationExpressionSyntax invoke && invoke.Expression is MemberAccessExpressionSyntax parentMemberAccess && parentMemberAccess.Name.Identifier.Text == nameof(Task.GetAwaiter); + var memberAccessExpression = blockingIdentifier?.Parent as MemberAccessExpressionSyntax; - if (!asyncAlternativeExists) - { - // If we fail to recognize the container, assume it exists since the analyzer thought it would. - ITypeSymbol? container = memberAccessExpression is object ? semanticModel.GetTypeInfo(memberAccessExpression.Expression, context.CancellationToken).ConvertedType : null; - asyncAlternativeExists = container is null || semanticModel.LookupSymbols(diagnostic.Location.SourceSpan.Start, name: asyncMethodName, container: container, includeReducedExtensionMethods: true).Any(); - } + // Check whether this code was already calling the awaiter (in a synchronous fashion). + asyncAlternativeExists |= memberAccessExpression?.Expression is InvocationExpressionSyntax invoke && invoke.Expression is MemberAccessExpressionSyntax parentMemberAccess && parentMemberAccess.Name.Identifier.Text == nameof(Task.GetAwaiter); - if (asyncAlternativeExists) - { - context.RegisterCodeFix(new ReplaceSyncMethodCallWithAwaitAsync(context.Document, diagnostic), diagnostic); - } + if (!asyncAlternativeExists) + { + // If we fail to recognize the container, assume it exists since the analyzer thought it would. + ITypeSymbol? container = memberAccessExpression is object ? semanticModel.GetTypeInfo(memberAccessExpression.Expression, context.CancellationToken).ConvertedType : null; + asyncAlternativeExists = container is null || semanticModel?.LookupSymbols(diagnostic.Location.SourceSpan.Start, name: asyncMethodName, container: container, includeReducedExtensionMethods: true).Any() is true; + } + + if (asyncAlternativeExists) + { + context.RegisterCodeFix(new ReplaceSyncMethodCallWithAwaitAsync(context.Document, diagnostic), diagnostic); } } + } - /// - public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + /// + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; - private class ReplaceSyncMethodCallWithAwaitAsync : CodeAction - { - private readonly Document document; - private readonly Diagnostic diagnostic; + private class ReplaceSyncMethodCallWithAwaitAsync : CodeAction + { + private readonly Document document; + private readonly Diagnostic diagnostic; - internal ReplaceSyncMethodCallWithAwaitAsync(Document document, Diagnostic diagnostic) - { - this.document = document; - this.diagnostic = diagnostic; - } + internal ReplaceSyncMethodCallWithAwaitAsync(Document document, Diagnostic diagnostic) + { + this.document = document; + this.diagnostic = diagnostic; + } - public override string Title + public override string Title + { + get { - get - { - return !string.IsNullOrEmpty(this.AlternativeAsyncMethod) - ? string.Format(CultureInfo.CurrentCulture, Strings.AwaitXInstead, this.AlternativeAsyncMethod) - : Strings.UseAwaitInstead; - } + return !string.IsNullOrEmpty(this.AlternativeAsyncMethod) + ? new LocalizableResourceString(nameof(Strings.AwaitXInstead), Strings.ResourceManager, typeof(string), this.AlternativeAsyncMethod!).ToString() + : Strings.UseAwaitInstead; } + } - /// - public override string? EquivalenceKey => null; + /// + public override string? EquivalenceKey => null; - private string AlternativeAsyncMethod => this.diagnostic.Properties[VSTHRD103UseAsyncOptionAnalyzer.AsyncMethodKeyName]; + private string? AlternativeAsyncMethod => this.diagnostic.Properties[VSTHRD103UseAsyncOptionAnalyzer.AsyncMethodKeyName]; - private string ExtensionMethodNamespace => this.diagnostic.Properties[VSTHRD103UseAsyncOptionAnalyzer.ExtensionMethodNamespaceKeyName]; + private string? ExtensionMethodNamespace => this.diagnostic.Properties[VSTHRD103UseAsyncOptionAnalyzer.ExtensionMethodNamespaceKeyName]; - protected override async Task GetChangedSolutionAsync(CancellationToken cancellationToken) + protected override async Task GetChangedSolutionAsync(CancellationToken cancellationToken) + { + Document? document = this.document; + SyntaxNode root = await document.GetSyntaxRootOrThrowAsync(cancellationToken).ConfigureAwait(false); + + // Find the synchronously blocking call member, + // and bookmark it so we can find it again after some mutations have taken place. + var syncAccessBookmark = new SyntaxAnnotation(); + SimpleNameSyntax syncMethodName = (SimpleNameSyntax)root.FindNode(this.diagnostic.Location.SourceSpan); + if (syncMethodName is null) { - Document? document = this.document; - SyntaxNode? root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - - // Find the synchronously blocking call member, - // and bookmark it so we can find it again after some mutations have taken place. - var syncAccessBookmark = new SyntaxAnnotation(); - SimpleNameSyntax syncMethodName = (SimpleNameSyntax)root.FindNode(this.diagnostic.Location.SourceSpan); - if (syncMethodName is null) - { - MemberAccessExpressionSyntax? syncMemberAccess = root.FindNode(this.diagnostic.Location.SourceSpan).FirstAncestorOrSelf(); - syncMethodName = syncMemberAccess.Name; - } + MemberAccessExpressionSyntax? syncMemberAccess = root.FindNode(this.diagnostic.Location.SourceSpan).FirstAncestorOrSelf(); + syncMethodName = syncMemberAccess?.Name ?? throw new InvalidOperationException("Unable to determine method name."); + } - // When we give the Document a modified SyntaxRoot, yet another is created. So we first assign it to the Document, - // then we query for the SyntaxRoot from the Document. - document = document.WithSyntaxRoot( - root.ReplaceNode(syncMethodName, syncMethodName.WithAdditionalAnnotations(syncAccessBookmark))); - root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - syncMethodName = (SimpleNameSyntax)root.GetAnnotatedNodes(syncAccessBookmark).Single(); - - // We'll need the semantic model later. But because we've annotated a node, that changes the SyntaxRoot - // and that renders the default semantic model broken (even though we've already updated the document's SyntaxRoot?!). - // So after acquiring the semantic model, update it with the new method body. - SemanticModel? semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - AnonymousFunctionExpressionSyntax? originalAnonymousMethodContainerIfApplicable = syncMethodName.FirstAncestorOrSelf(); - MethodDeclarationSyntax? originalMethodDeclaration = syncMethodName.FirstAncestorOrSelf(); - - ISymbol? enclosingSymbol = semanticModel.GetEnclosingSymbol(this.diagnostic.Location.SourceSpan.Start, cancellationToken); - var hasReturnValue = ((enclosingSymbol as IMethodSymbol)?.ReturnType as INamedTypeSymbol)?.IsGenericType ?? false; - - // Ensure that the method or anonymous delegate is using the async keyword. - MethodDeclarationSyntax updatedMethod; - if (originalAnonymousMethodContainerIfApplicable is object) - { - updatedMethod = originalMethodDeclaration.ReplaceNode( - originalAnonymousMethodContainerIfApplicable, - originalAnonymousMethodContainerIfApplicable.MakeMethodAsync(hasReturnValue, semanticModel, cancellationToken)); - } - else - { - (document, updatedMethod) = await originalMethodDeclaration.MakeMethodAsync(document, cancellationToken).ConfigureAwait(false); - semanticModel = null; // out-dated - } + // When we give the Document a modified SyntaxRoot, yet another is created. So we first assign it to the Document, + // then we query for the SyntaxRoot from the Document. + document = document.WithSyntaxRoot( + root.ReplaceNode(syncMethodName, syncMethodName.WithAdditionalAnnotations(syncAccessBookmark))); + root = await document.GetSyntaxRootOrThrowAsync(cancellationToken).ConfigureAwait(false); + syncMethodName = (SimpleNameSyntax)root.GetAnnotatedNodes(syncAccessBookmark).Single(); + + // We'll need the semantic model later. But because we've annotated a node, that changes the SyntaxRoot + // and that renders the default semantic model broken (even though we've already updated the document's SyntaxRoot?!). + // So after acquiring the semantic model, update it with the new method body. + SemanticModel? semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + AnonymousFunctionExpressionSyntax? originalAnonymousMethodContainerIfApplicable = syncMethodName.FirstAncestorOrSelf(); + MethodDeclarationSyntax originalMethodDeclaration = syncMethodName.FirstAncestorOrSelf() ?? throw new InvalidOperationException("Unable to find containing method."); + + ISymbol? enclosingSymbol = semanticModel?.GetEnclosingSymbol(this.diagnostic.Location.SourceSpan.Start, cancellationToken); + var hasReturnValue = ((enclosingSymbol as IMethodSymbol)?.ReturnType as INamedTypeSymbol)?.IsGenericType ?? false; + + // Ensure that the method or anonymous delegate is using the async keyword. + MethodDeclarationSyntax? updatedMethod; + if (originalAnonymousMethodContainerIfApplicable is object) + { + updatedMethod = originalMethodDeclaration.ReplaceNode( + originalAnonymousMethodContainerIfApplicable, + originalAnonymousMethodContainerIfApplicable.MakeMethodAsync(hasReturnValue, semanticModel, cancellationToken)); + } + else + { + (document, updatedMethod) = await originalMethodDeclaration.MakeMethodAsync(document, cancellationToken).ConfigureAwait(false); + semanticModel = null; // out-dated + } - if (updatedMethod != originalMethodDeclaration) - { - // Re-discover our synchronously blocking member. - syncMethodName = (SimpleNameSyntax)updatedMethod.GetAnnotatedNodes(syncAccessBookmark).Single(); - } + if (updatedMethod != originalMethodDeclaration) + { + // Re-discover our synchronously blocking member. + syncMethodName = (SimpleNameSyntax)updatedMethod.GetAnnotatedNodes(syncAccessBookmark).Single(); + } - ExpressionSyntax? syncExpression = GetSynchronousExpression(syncMethodName); + ExpressionSyntax syncExpression = GetSynchronousExpression(syncMethodName) ?? throw new InvalidOperationException("Unable to find sync expression."); - ExpressionSyntax awaitExpression; - if (!string.IsNullOrEmpty(this.AlternativeAsyncMethod)) - { - // Replace the member being called and await the invocation expression. - // While doing so, move leading trivia to the surrounding await expression. - SimpleNameSyntax? asyncMethodName = syncMethodName.WithIdentifier(SyntaxFactory.Identifier(this.diagnostic.Properties[VSTHRD103UseAsyncOptionAnalyzer.AsyncMethodKeyName])); - awaitExpression = SyntaxFactory.AwaitExpression( - syncExpression.ReplaceNode(syncMethodName, asyncMethodName).WithoutLeadingTrivia()) - .WithLeadingTrivia(syncExpression.GetLeadingTrivia()); - } - else - { - // Remove the member being accessed that causes a synchronous block and simply await the object. - MemberAccessExpressionSyntax? syncMemberAccess = syncMethodName.FirstAncestorOrSelf(); - ExpressionSyntax? syncMemberStrippedExpression = syncMemberAccess.Expression; - - // Special case a common pattern of calling task.GetAwaiter().GetResult() and remove both method calls. - var expressionMethodCall = (syncMemberStrippedExpression as InvocationExpressionSyntax)?.Expression as MemberAccessExpressionSyntax; - if (expressionMethodCall?.Name.Identifier.Text == nameof(Task.GetAwaiter)) - { - syncMemberStrippedExpression = expressionMethodCall.Expression; - } - - awaitExpression = SyntaxFactory.AwaitExpression(syncMemberStrippedExpression.WithoutLeadingTrivia()) - .WithLeadingTrivia(syncMemberStrippedExpression.GetLeadingTrivia()); - } + ExpressionSyntax awaitExpression; + if (!string.IsNullOrEmpty(this.AlternativeAsyncMethod)) + { + // Replace the member being called and await the invocation expression. + // While doing so, move leading trivia to the surrounding await expression. + SimpleNameSyntax? asyncMethodName = syncMethodName.WithIdentifier(SyntaxFactory.Identifier(this.diagnostic.Properties[VSTHRD103UseAsyncOptionAnalyzer.AsyncMethodKeyName]!)); + awaitExpression = SyntaxFactory.AwaitExpression( + syncExpression.ReplaceNode(syncMethodName, asyncMethodName).WithoutLeadingTrivia()) + .WithLeadingTrivia(syncExpression.GetLeadingTrivia()); + } + else + { + // Remove the member being accessed that causes a synchronous block and simply await the object. + MemberAccessExpressionSyntax syncMemberAccess = syncMethodName.FirstAncestorOrSelf() ?? throw new InvalidOperationException("Unable to find member access expression."); + ExpressionSyntax? syncMemberStrippedExpression = syncMemberAccess.Expression; - if (!(syncExpression.Parent is ExpressionStatementSyntax)) + // Special case a common pattern of calling task.GetAwaiter().GetResult() and remove both method calls. + var expressionMethodCall = (syncMemberStrippedExpression as InvocationExpressionSyntax)?.Expression as MemberAccessExpressionSyntax; + if (expressionMethodCall?.Name.Identifier.Text == nameof(Task.GetAwaiter)) { - awaitExpression = SyntaxFactory.ParenthesizedExpression(awaitExpression) - .WithAdditionalAnnotations(Simplifier.Annotation); + syncMemberStrippedExpression = expressionMethodCall.Expression; } - updatedMethod = updatedMethod - .ReplaceNode(syncExpression, awaitExpression); + awaitExpression = SyntaxFactory.AwaitExpression(syncMemberStrippedExpression.WithoutLeadingTrivia()) + .WithLeadingTrivia(syncMemberStrippedExpression.GetLeadingTrivia()); + } - SyntaxNode? newRoot = root.ReplaceNode(originalMethodDeclaration, updatedMethod); - Document? newDocument = document.WithSyntaxRoot(newRoot); - return newDocument.Project.Solution; + if (!(syncExpression.Parent is ExpressionStatementSyntax)) + { + awaitExpression = SyntaxFactory.ParenthesizedExpression(awaitExpression) + .WithAdditionalAnnotations(Simplifier.Annotation); } - private static ExpressionSyntax GetSynchronousExpression(SimpleNameSyntax syncMethodName) + updatedMethod = updatedMethod + .ReplaceNode(syncExpression, awaitExpression); + + SyntaxNode? newRoot = root.ReplaceNode(originalMethodDeclaration, updatedMethod); + Document? newDocument = document.WithSyntaxRoot(newRoot); + return newDocument.Project.Solution; + } + + private static ExpressionSyntax? GetSynchronousExpression(SimpleNameSyntax? syncMethodName) + { + SyntaxNode? current = syncMethodName; + while (true) { - SyntaxNode current = syncMethodName; - while (true) + switch (current?.Kind()) { - switch (current.Kind()) - { - case SyntaxKind.InvocationExpression: + case SyntaxKind.InvocationExpression: + return (ExpressionSyntax)current; + + case SyntaxKind.SimpleMemberAccessExpression: + if (current.Parent.IsKind(SyntaxKind.InvocationExpression)) + { + return (ExpressionSyntax)current.Parent; + } + else + { return (ExpressionSyntax)current; + } + + case null: + return null; - case SyntaxKind.SimpleMemberAccessExpression: - if (current.Parent.IsKind(SyntaxKind.InvocationExpression)) - { - return (ExpressionSyntax)current.Parent; - } - else - { - return (ExpressionSyntax)current; - } - - default: - current = current.Parent; - break; - } + default: + current = current.Parent; + break; } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD107AwaitTaskWithinUsingExpressionCodeFix.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD107AwaitTaskWithinUsingExpressionCodeFix.cs index c143b1a26..a399d0ec4 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD107AwaitTaskWithinUsingExpressionCodeFix.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD107AwaitTaskWithinUsingExpressionCodeFix.cs @@ -1,88 +1,87 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Globalization; - using System.Linq; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CodeActions; - using Microsoft.CodeAnalysis.CodeFixes; - using Microsoft.CodeAnalysis.CSharp; - using Microsoft.CodeAnalysis.CSharp.Syntax; - using Microsoft.CodeAnalysis.Simplification; - using Microsoft.VisualStudio.Threading; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Simplification; +using Microsoft.VisualStudio.Threading; - /// - /// Offers a code fix for diagnostics produced by the - /// . - /// - /// - /// The code fix changes code like this as described: - /// - /// - /// - /// - [ExportCodeFixProvider(LanguageNames.CSharp)] - public class VSTHRD107AwaitTaskWithinUsingExpressionCodeFix : CodeFixProvider - { - private static readonly ImmutableArray ReusableFixableDiagnosticIds = ImmutableArray.Create( - VSTHRD107AwaitTaskWithinUsingExpressionAnalyzer.Id); +namespace Microsoft.VisualStudio.Threading.Analyzers; - public override ImmutableArray FixableDiagnosticIds => ReusableFixableDiagnosticIds; +/// +/// Offers a code fix for diagnostics produced by the +/// . +/// +/// +/// The code fix changes code like this as described: +/// +/// +/// +/// +[ExportCodeFixProvider(LanguageNames.CSharp)] +public class VSTHRD107AwaitTaskWithinUsingExpressionCodeFix : CodeFixProvider +{ + private static readonly ImmutableArray ReusableFixableDiagnosticIds = ImmutableArray.Create( + VSTHRD107AwaitTaskWithinUsingExpressionAnalyzer.Id); - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - Diagnostic? diagnostic = context.Diagnostics.First(); + public override ImmutableArray FixableDiagnosticIds => ReusableFixableDiagnosticIds; - context.RegisterCodeFix( - CodeAction.Create( - Strings.VSTHRD107_CodeFix_Title, - async ct => - { - Document? document = context.Document; - SyntaxNode? root = await document.GetSyntaxRootAsync(ct).ConfigureAwait(false); - MethodDeclarationSyntax? method = root.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf(); + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + Diagnostic? diagnostic = context.Diagnostics.First(); - (document, method, _) = await FixUtils.UpdateDocumentAsync( - document, - method, - m => - { - root = m.SyntaxTree.GetRoot(ct); - UsingStatementSyntax usingStatement = root.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf(); - AwaitExpressionSyntax awaitExpression = SyntaxFactory.AwaitExpression( - SyntaxFactory.ParenthesizedExpression(usingStatement.Expression)); - UsingStatementSyntax modifiedUsingStatement = usingStatement.WithExpression(awaitExpression) - .WithAdditionalAnnotations(Simplifier.Annotation); - return m.ReplaceNode(usingStatement, modifiedUsingStatement); - }, - ct).ConfigureAwait(false); - (document, method) = await method.MakeMethodAsync(document, ct).ConfigureAwait(false); + context.RegisterCodeFix( + CodeAction.Create( + Strings.VSTHRD107_CodeFix_Title, + async ct => + { + Document? document = context.Document; + SyntaxNode? root = await document.GetSyntaxRootOrThrowAsync(ct).ConfigureAwait(false); + MethodDeclarationSyntax method = root.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf() ?? throw new InvalidOperationException("Unable to find MethodDeclaration."); - return document.Project.Solution; - }, - "only action"), - diagnostic); + (document, method, _) = await FixUtils.UpdateDocumentAsync( + document, + method, + m => + { + root = m.SyntaxTree.GetRoot(ct); + UsingStatementSyntax? usingStatement = root.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf() ?? throw new InvalidOperationException("Unable to find using statement."); + AwaitExpressionSyntax awaitExpression = SyntaxFactory.AwaitExpression( + SyntaxFactory.ParenthesizedExpression(usingStatement.Expression!)); + UsingStatementSyntax modifiedUsingStatement = usingStatement.WithExpression(awaitExpression) + .WithAdditionalAnnotations(Simplifier.Annotation); + return m.ReplaceNode(usingStatement, modifiedUsingStatement); + }, + ct).ConfigureAwait(false); + (document, method) = await method.MakeMethodAsync(document, ct).ConfigureAwait(false); - return Task.FromResult(null); - } + return document.Project.Solution; + }, + "only action"), + diagnostic); - /// - public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + return Task.FromResult(null); } + + /// + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD109AvoidAssertInAsyncMethodsCodeFix.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD109AvoidAssertInAsyncMethodsCodeFix.cs index 662c3dd19..d9ecff843 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD109AvoidAssertInAsyncMethodsCodeFix.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD109AvoidAssertInAsyncMethodsCodeFix.cs @@ -1,159 +1,158 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Simplification; +using Microsoft.VisualStudio.Threading; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +[ExportCodeFixProvider(LanguageNames.CSharp)] +public class VSTHRD109AvoidAssertInAsyncMethodsCodeFix : CodeFixProvider { - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Linq; - using System.Text; - using System.Text.RegularExpressions; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CodeActions; - using Microsoft.CodeAnalysis.CodeFixes; - using Microsoft.CodeAnalysis.CSharp; - using Microsoft.CodeAnalysis.CSharp.Syntax; - using Microsoft.CodeAnalysis.Formatting; - using Microsoft.CodeAnalysis.Simplification; - using Microsoft.VisualStudio.Threading; - - [ExportCodeFixProvider(LanguageNames.CSharp)] - public class VSTHRD109AvoidAssertInAsyncMethodsCodeFix : CodeFixProvider - { - private static readonly ImmutableArray ReusableFixableDiagnosticIds = ImmutableArray.Create( - AbstractVSTHRD109AvoidAssertInAsyncMethodsAnalyzer.Id); + private static readonly ImmutableArray ReusableFixableDiagnosticIds = ImmutableArray.Create( + AbstractVSTHRD109AvoidAssertInAsyncMethodsAnalyzer.Id); - public override ImmutableArray FixableDiagnosticIds => ReusableFixableDiagnosticIds; + public override ImmutableArray FixableDiagnosticIds => ReusableFixableDiagnosticIds; - /// - public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + /// + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; - public override async Task RegisterCodeFixesAsync(CodeFixContext context) + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + foreach (Diagnostic? diagnostic in context.Diagnostics) { - foreach (Diagnostic? diagnostic in context.Diagnostics) + SyntaxNode root = await context.Document.GetSyntaxRootOrThrowAsync(context.CancellationToken).ConfigureAwait(false); + ExpressionSyntax? syntaxNode = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true) as ExpressionSyntax; + if (syntaxNode is null) { - SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - ExpressionSyntax? syntaxNode = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true) as ExpressionSyntax; - if (syntaxNode is null) - { - continue; - } + continue; + } - CSharpUtils.ContainingFunctionData container = CSharpUtils.GetContainingFunction(syntaxNode); - if (container.BlockOrExpression is null) + CSharpUtils.ContainingFunctionData container = CSharpUtils.GetContainingFunction(syntaxNode); + if (container.BlockOrExpression is null) + { + return; + } + + if (!container.IsAsync) + { + if (!(container.Function is MethodDeclarationSyntax || container.Function is AnonymousFunctionExpressionSyntax)) { + // We don't support converting whatever this is into an async method. return; } + } - if (!container.IsAsync) + SemanticModel? semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + ISymbol? enclosingSymbol = semanticModel?.GetEnclosingSymbol(diagnostic.Location.SourceSpan.Start, context.CancellationToken); + if (enclosingSymbol is null) + { + return; + } + + var hasReturnValue = ((enclosingSymbol as IMethodSymbol)?.ReturnType as INamedTypeSymbol)?.IsGenericType ?? false; + ImmutableArray options = await CommonFixes.ReadMethodsAsync(context, CommonInterest.FileNamePatternForMethodsThatSwitchToMainThread, context.CancellationToken); + int positionForLookup = diagnostic.Location.SourceSpan.Start; + ISymbol cancellationTokenSymbol = Utils.FindCancellationToken(semanticModel, positionForLookup, context.CancellationToken).FirstOrDefault(); + foreach (CommonInterest.QualifiedMember option in options) + { + // We're looking for methods that either require no parameters, + // or (if we have one to give) that have just one parameter that is a CancellationToken. + IMethodSymbol? proposedMethod = semanticModel is null ? null : Utils.FindMethodGroup(semanticModel, option) + .FirstOrDefault(m => !m.Parameters.Any(p => !p.HasExplicitDefaultValue) || + (cancellationTokenSymbol is object && m.Parameters.Length == 1 && Utils.IsCancellationTokenParameter(m.Parameters[0]))); + if (proposedMethod is null) { - if (!(container.Function is MethodDeclarationSyntax || container.Function is AnonymousFunctionExpressionSyntax)) - { - // We don't support converting whatever this is into an async method. - return; - } + // We can't find it, so don't offer to use it. + continue; } - SemanticModel? semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); - ISymbol? enclosingSymbol = semanticModel.GetEnclosingSymbol(diagnostic.Location.SourceSpan.Start, context.CancellationToken); - if (enclosingSymbol is null) + if (proposedMethod.IsStatic) { - return; + OfferFix(option.ToString()); } - - var hasReturnValue = ((enclosingSymbol as IMethodSymbol)?.ReturnType as INamedTypeSymbol)?.IsGenericType ?? false; - ImmutableArray options = await CommonFixes.ReadMethodsAsync(context, CommonInterest.FileNamePatternForMethodsThatSwitchToMainThread, context.CancellationToken); - int positionForLookup = diagnostic.Location.SourceSpan.Start; - ISymbol cancellationTokenSymbol = Utils.FindCancellationToken(semanticModel, positionForLookup, context.CancellationToken).FirstOrDefault(); - foreach (CommonInterest.QualifiedMember option in options) + else if (semanticModel is not null) { - // We're looking for methods that either require no parameters, - // or (if we have one to give) that have just one parameter that is a CancellationToken. - IMethodSymbol? proposedMethod = Utils.FindMethodGroup(semanticModel, option) - .FirstOrDefault(m => !m.Parameters.Any(p => !p.HasExplicitDefaultValue) || - (cancellationTokenSymbol is object && m.Parameters.Length == 1 && Utils.IsCancellationTokenParameter(m.Parameters[0]))); - if (proposedMethod is null) - { - // We can't find it, so don't offer to use it. - continue; - } - - if (proposedMethod.IsStatic) - { - OfferFix(option.ToString()); - } - else + foreach (Tuple? candidate in Utils.FindInstanceOf(proposedMethod.ContainingType, semanticModel, positionForLookup, context.CancellationToken)) { - foreach (Tuple? candidate in Utils.FindInstanceOf(proposedMethod.ContainingType, semanticModel, positionForLookup, context.CancellationToken)) + if (candidate.Item1) { - if (candidate.Item1) - { - OfferFix($"{candidate.Item2.Name}.{proposedMethod.Name}"); - } - else - { - OfferFix($"{candidate.Item2.ContainingNamespace}.{candidate.Item2.ContainingType.Name}.{candidate.Item2.Name}.{proposedMethod.Name}"); - } + OfferFix($"{candidate.Item2.Name}.{proposedMethod.Name}"); + } + else + { + OfferFix($"{candidate.Item2.ContainingNamespace}.{candidate.Item2.ContainingType.Name}.{candidate.Item2.Name}.{proposedMethod.Name}"); } } + } - void OfferFix(string fullyQualifiedMethod) - { - context.RegisterCodeFix(CodeAction.Create($"Use 'await {fullyQualifiedMethod}'", ct => Fix(fullyQualifiedMethod, proposedMethod, hasReturnValue, ct), fullyQualifiedMethod), context.Diagnostics); - } + void OfferFix(string fullyQualifiedMethod) + { + context.RegisterCodeFix(CodeAction.Create($"Use 'await {fullyQualifiedMethod}'", ct => Fix(fullyQualifiedMethod, proposedMethod, hasReturnValue, ct), fullyQualifiedMethod), context.Diagnostics); } + } - async Task Fix(string fullyQualifiedMethod, IMethodSymbol methodSymbol, bool hasReturnValue, CancellationToken cancellationToken) + async Task Fix(string fullyQualifiedMethod, IMethodSymbol methodSymbol, bool hasReturnValue, CancellationToken cancellationToken) + { + StatementSyntax assertionStatementToRemove = syntaxNode!.FirstAncestorOrSelf() ?? throw new InvalidOperationException("Unable to find containing statement."); + + int typeAndMethodDelimiterIndex = fullyQualifiedMethod.LastIndexOf('.'); + IdentifierNameSyntax methodName = SyntaxFactory.IdentifierName(fullyQualifiedMethod.Substring(typeAndMethodDelimiterIndex + 1)); + ExpressionSyntax invokedMethod = CSharpUtils.MemberAccess(fullyQualifiedMethod.Substring(0, typeAndMethodDelimiterIndex).Split('.'), methodName) + .WithAdditionalAnnotations(Simplifier.Annotation); + InvocationExpressionSyntax? invocationExpression = SyntaxFactory.InvocationExpression(invokedMethod); + IParameterSymbol? cancellationTokenParameter = methodSymbol.Parameters.FirstOrDefault(Utils.IsCancellationTokenParameter); + if (cancellationTokenParameter is object && cancellationTokenSymbol is object) { - StatementSyntax? assertionStatementToRemove = syntaxNode!.FirstAncestorOrSelf(); - - int typeAndMethodDelimiterIndex = fullyQualifiedMethod.LastIndexOf('.'); - IdentifierNameSyntax methodName = SyntaxFactory.IdentifierName(fullyQualifiedMethod.Substring(typeAndMethodDelimiterIndex + 1)); - ExpressionSyntax invokedMethod = CSharpUtils.MemberAccess(fullyQualifiedMethod.Substring(0, typeAndMethodDelimiterIndex).Split('.'), methodName) - .WithAdditionalAnnotations(Simplifier.Annotation); - InvocationExpressionSyntax? invocationExpression = SyntaxFactory.InvocationExpression(invokedMethod); - IParameterSymbol? cancellationTokenParameter = methodSymbol.Parameters.FirstOrDefault(Utils.IsCancellationTokenParameter); - if (cancellationTokenParameter is object && cancellationTokenSymbol is object) + ArgumentSyntax? arg = SyntaxFactory.Argument(SyntaxFactory.IdentifierName(cancellationTokenSymbol.Name)); + if (methodSymbol.Parameters.IndexOf(cancellationTokenParameter) > 0) { - ArgumentSyntax? arg = SyntaxFactory.Argument(SyntaxFactory.IdentifierName(cancellationTokenSymbol.Name)); - if (methodSymbol.Parameters.IndexOf(cancellationTokenParameter) > 0) - { - arg = arg.WithNameColon(SyntaxFactory.NameColon(SyntaxFactory.IdentifierName(cancellationTokenParameter.Name))); - } - - invocationExpression = invocationExpression.AddArgumentListArguments(arg); + arg = arg.WithNameColon(SyntaxFactory.NameColon(SyntaxFactory.IdentifierName(cancellationTokenParameter.Name))); } - ExpressionSyntax awaitExpression = SyntaxFactory.AwaitExpression(invocationExpression); - ExpressionStatementSyntax? addedStatement = SyntaxFactory.ExpressionStatement(awaitExpression) - .WithAdditionalAnnotations(Simplifier.Annotation, Formatter.Annotation); - - var methodAnnotation = new SyntaxAnnotation(); - CSharpSyntaxNode methodSyntax = container.Function.ReplaceNode(assertionStatementToRemove, addedStatement) - .WithAdditionalAnnotations(methodAnnotation); - Document newDocument = context.Document.WithSyntaxRoot(root.ReplaceNode(container.Function, methodSyntax)); - SyntaxNode? newSyntaxRoot = await newDocument.GetSyntaxRootAsync(cancellationToken); - methodSyntax = (CSharpSyntaxNode)newSyntaxRoot.GetAnnotatedNodes(methodAnnotation).Single(); - if (!container.IsAsync) + invocationExpression = invocationExpression.AddArgumentListArguments(arg); + } + + ExpressionSyntax awaitExpression = SyntaxFactory.AwaitExpression(invocationExpression); + ExpressionStatementSyntax? addedStatement = SyntaxFactory.ExpressionStatement(awaitExpression) + .WithAdditionalAnnotations(Simplifier.Annotation, Formatter.Annotation); + + var methodAnnotation = new SyntaxAnnotation(); + CSharpSyntaxNode methodSyntax = container.Function.ReplaceNode(assertionStatementToRemove, addedStatement) + .WithAdditionalAnnotations(methodAnnotation); + Document newDocument = context.Document.WithSyntaxRoot(root.ReplaceNode(container.Function, methodSyntax)); + SyntaxNode? newSyntaxRoot = await newDocument.GetSyntaxRootOrThrowAsync(cancellationToken); + methodSyntax = (CSharpSyntaxNode)newSyntaxRoot.GetAnnotatedNodes(methodAnnotation).Single(); + if (!container.IsAsync) + { + switch (methodSyntax) { - switch (methodSyntax) - { - case AnonymousFunctionExpressionSyntax anonFunc: - semanticModel = await newDocument.GetSemanticModelAsync(cancellationToken); - methodSyntax = FixUtils.MakeMethodAsync(anonFunc, hasReturnValue, semanticModel, cancellationToken); - newDocument = newDocument.WithSyntaxRoot(newSyntaxRoot.ReplaceNode(anonFunc, methodSyntax)); - break; - case MethodDeclarationSyntax methodDecl: - (newDocument, methodSyntax) = await FixUtils.MakeMethodAsync(methodDecl, newDocument, cancellationToken); - break; - } + case AnonymousFunctionExpressionSyntax anonFunc: + semanticModel = await newDocument.GetSemanticModelAsync(cancellationToken); + methodSyntax = FixUtils.MakeMethodAsync(anonFunc, hasReturnValue, semanticModel, cancellationToken); + newDocument = newDocument.WithSyntaxRoot(newSyntaxRoot.ReplaceNode(anonFunc, methodSyntax)); + break; + case MethodDeclarationSyntax methodDecl: + (newDocument, methodSyntax) = await FixUtils.MakeMethodAsync(methodDecl, newDocument, cancellationToken); + break; } - - return newDocument.Project.Solution; } + + return newDocument.Project.Solution; } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD111UseConfigureAwaitCodeFix.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD111UseConfigureAwaitCodeFix.cs index 785c0ddc2..658a20e07 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD111UseConfigureAwaitCodeFix.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD111UseConfigureAwaitCodeFix.cs @@ -1,61 +1,64 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Simplification; +using Microsoft.VisualStudio.Threading; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +[ExportCodeFixProvider(LanguageNames.CSharp)] +public class VSTHRD111UseConfigureAwaitCodeFix : CodeFixProvider { - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Globalization; - using System.Linq; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CodeActions; - using Microsoft.CodeAnalysis.CodeFixes; - using Microsoft.CodeAnalysis.CSharp; - using Microsoft.CodeAnalysis.CSharp.Syntax; - using Microsoft.CodeAnalysis.Simplification; - using Microsoft.VisualStudio.Threading; - - [ExportCodeFixProvider(LanguageNames.CSharp)] - public class VSTHRD111UseConfigureAwaitCodeFix : CodeFixProvider - { - private static readonly ImmutableArray ReusableFixableDiagnosticIds = ImmutableArray.Create( - VSTHRD111UseConfigureAwaitAnalyzer.Id); + private static readonly ImmutableArray ReusableFixableDiagnosticIds = ImmutableArray.Create( + VSTHRD111UseConfigureAwaitAnalyzer.Id); - /// - public override ImmutableArray FixableDiagnosticIds => ReusableFixableDiagnosticIds; + /// + public override ImmutableArray FixableDiagnosticIds => ReusableFixableDiagnosticIds; - /// - public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + /// + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; - public override async Task RegisterCodeFixesAsync(CodeFixContext context) + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + foreach (Diagnostic? diagnostic in context.Diagnostics) { - foreach (Diagnostic? diagnostic in context.Diagnostics) + SemanticModel? semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + SyntaxNode syntaxRoot = await context.Document.GetSyntaxRootOrThrowAsync(context.CancellationToken).ConfigureAwait(false); + var awaitedExpression = syntaxRoot.FindNode(diagnostic.Location.SourceSpan) as ExpressionSyntax; + if (awaitedExpression is null) { - SemanticModel? semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); - SyntaxNode? syntaxRoot = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - var awaitedExpression = syntaxRoot.FindNode(diagnostic.Location.SourceSpan) as ExpressionSyntax; - - Task ApplyFix(bool captureContext) - { - ExpressionSyntax configuredAwaitExpression = SyntaxFactory.ParenthesizedExpression( - SyntaxFactory.InvocationExpression( - SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - SyntaxFactory.ParenthesizedExpression(awaitedExpression).WithAdditionalAnnotations(Simplifier.Annotation), - SyntaxFactory.IdentifierName("ConfigureAwait"))) - .AddArgumentListArguments(SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(captureContext ? SyntaxKind.TrueLiteralExpression : SyntaxKind.FalseLiteralExpression)))) - .WithAdditionalAnnotations(Simplifier.Annotation); - - return Task.FromResult(context.Document.WithSyntaxRoot(syntaxRoot.ReplaceNode(awaitedExpression, configuredAwaitExpression))); - } - - context.RegisterCodeFix(CodeAction.Create(Strings.VSTHRD111_CodeFix_True_Title, ct => ApplyFix(true), true.ToString()), diagnostic); - context.RegisterCodeFix(CodeAction.Create(Strings.VSTHRD111_CodeFix_False_Title, ct => ApplyFix(false), false.ToString()), diagnostic); + return; } + + Task ApplyFix(bool captureContext) + { + ExpressionSyntax configuredAwaitExpression = SyntaxFactory.ParenthesizedExpression( + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ParenthesizedExpression(awaitedExpression).WithAdditionalAnnotations(Simplifier.Annotation), + SyntaxFactory.IdentifierName("ConfigureAwait"))) + .AddArgumentListArguments(SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(captureContext ? SyntaxKind.TrueLiteralExpression : SyntaxKind.FalseLiteralExpression)))) + .WithAdditionalAnnotations(Simplifier.Annotation); + + return Task.FromResult(context.Document.WithSyntaxRoot(syntaxRoot.ReplaceNode(awaitedExpression, configuredAwaitExpression))); + } + + context.RegisterCodeFix(CodeAction.Create(Strings.VSTHRD111_CodeFix_False_Title, ct => ApplyFix(false), false.ToString()), diagnostic); + context.RegisterCodeFix(CodeAction.Create(Strings.VSTHRD111_CodeFix_True_Title, ct => ApplyFix(true), true.ToString()), diagnostic); } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD112ImplementSystemIAsyncDisposableCodeFix.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD112ImplementSystemIAsyncDisposableCodeFix.cs index 6762b7c65..8ce2747ce 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD112ImplementSystemIAsyncDisposableCodeFix.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD112ImplementSystemIAsyncDisposableCodeFix.cs @@ -1,89 +1,87 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using System.Collections.Immutable; - using System.Linq; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CodeActions; - using Microsoft.CodeAnalysis.CodeFixes; - using Microsoft.CodeAnalysis.Editing; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Editing; - [ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic)] - public class VSTHRD112ImplementSystemIAsyncDisposableCodeFix : CodeFixProvider - { - private static readonly ImmutableArray ReusableFixableDiagnosticIds = ImmutableArray.Create( - AbstractVSTHRD112ImplementSystemIAsyncDisposableAnalyzer.Id); +namespace Microsoft.VisualStudio.Threading.Analyzers; + +[ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic)] +public class VSTHRD112ImplementSystemIAsyncDisposableCodeFix : CodeFixProvider +{ + private static readonly ImmutableArray ReusableFixableDiagnosticIds = ImmutableArray.Create( + AbstractVSTHRD112ImplementSystemIAsyncDisposableAnalyzer.Id); - public override ImmutableArray FixableDiagnosticIds => ReusableFixableDiagnosticIds; + public override ImmutableArray FixableDiagnosticIds => ReusableFixableDiagnosticIds; - /// - public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + /// + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; - public override async Task RegisterCodeFixesAsync(CodeFixContext context) + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + foreach (Diagnostic? diagnostic in context.Diagnostics) { - foreach (Diagnostic? diagnostic in context.Diagnostics) + SemanticModel? semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken); + Compilation? compilation = await context.Document.Project.GetCompilationAsync(context.CancellationToken); + if (compilation is null) { - SemanticModel? semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken); - Compilation? compilation = await context.Document.Project.GetCompilationAsync(context.CancellationToken); - if (compilation is null) - { - continue; - } + continue; + } - INamedTypeSymbol? bclAsyncDisposableType = compilation.GetTypeByMetadataName(Types.BclAsyncDisposable.FullName); - if (bclAsyncDisposableType is null) - { - continue; - } + INamedTypeSymbol? bclAsyncDisposableType = compilation.GetTypeByMetadataName(Types.BclAsyncDisposable.FullName); + if (bclAsyncDisposableType is null) + { + continue; + } + + SyntaxNode syntaxRoot = await context.Document.GetSyntaxRootOrThrowAsync(context.CancellationToken); + var generator = SyntaxGenerator.GetGenerator(context.Document); + SyntaxNode? originalTypeDeclaration = generator.TryGetContainingDeclaration(syntaxRoot.FindNode(diagnostic.Location.SourceSpan)); + if (originalTypeDeclaration is null) + { + continue; + } - SyntaxNode? syntaxRoot = await context.Document.GetSyntaxRootAsync(context.CancellationToken); - var generator = SyntaxGenerator.GetGenerator(context.Document); - SyntaxNode? originalTypeDeclaration = generator.TryGetContainingDeclaration(syntaxRoot.FindNode(diagnostic.Location.SourceSpan)); - if (originalTypeDeclaration is null) - { - continue; - } + context.RegisterCodeFix( + CodeAction.Create( + Strings.VSTHRD112_CodeFix_Title, + ct => + { + // Declare that the type implements the System.IAsyncDisposable interface. + SyntaxNode? newBaseType = generator.TypeExpression(bclAsyncDisposableType); + SyntaxNode? typeDeclaration = generator.AddInterfaceType(originalTypeDeclaration, newBaseType); - context.RegisterCodeFix( - CodeAction.Create( - Strings.VSTHRD112_CodeFix_Title, - ct => + // Implement the interface, if we're on a non-interface type. + if (semanticModel?.GetDeclaredSymbol(originalTypeDeclaration, ct) is ITypeSymbol changedSymbol && changedSymbol.TypeKind != TypeKind.Interface) { - // Declare that the type implements the System.IAsyncDisposable interface. - SyntaxNode? newBaseType = generator.TypeExpression(bclAsyncDisposableType); - SyntaxNode? typeDeclaration = generator.AddInterfaceType(originalTypeDeclaration, newBaseType); - - // Implement the interface, if we're on a non-interface type. - if (semanticModel.GetDeclaredSymbol(originalTypeDeclaration, ct) is ITypeSymbol changedSymbol && changedSymbol.TypeKind != TypeKind.Interface) + var disposeAsyncMethod = (IMethodSymbol)bclAsyncDisposableType.GetMembers().Single(); + var statements = new SyntaxNode[] { - var disposeAsyncMethod = (IMethodSymbol)bclAsyncDisposableType.GetMembers().Single(); - var statements = new SyntaxNode[] - { - generator.ReturnStatement( - generator.ObjectCreationExpression( - disposeAsyncMethod.ReturnType, - generator.InvocationExpression( - generator.MemberAccessExpression(generator.ThisExpression(), "DisposeAsync")))), - }; - typeDeclaration = generator.AddMembers( - typeDeclaration, - generator.AsPrivateInterfaceImplementation( - generator.MethodDeclaration( - disposeAsyncMethod.Name, - returnType: generator.TypeExpression(disposeAsyncMethod.ReturnType), - accessibility: Accessibility.Public, - statements: statements), - generator.TypeExpression(bclAsyncDisposableType))); - } + generator.ReturnStatement( + generator.ObjectCreationExpression( + disposeAsyncMethod.ReturnType, + generator.InvocationExpression( + generator.MemberAccessExpression(generator.ThisExpression(), "DisposeAsync")))), + }; + SyntaxNode privateInterfaceMember = generator.AsPrivateInterfaceImplementation( + generator.MethodDeclaration( + disposeAsyncMethod.Name, + returnType: generator.TypeExpression(disposeAsyncMethod.ReturnType), + accessibility: Accessibility.Public, + statements: statements), + generator.TypeExpression(bclAsyncDisposableType))!; + typeDeclaration = generator.AddMembers(typeDeclaration, privateInterfaceMember); + } - return Task.FromResult(context.Document.WithSyntaxRoot(syntaxRoot.ReplaceNode(originalTypeDeclaration, typeDeclaration))); - }, - "AddBaseType"), - diagnostic); - } + return Task.FromResult(context.Document.WithSyntaxRoot(syntaxRoot.ReplaceNode(originalTypeDeclaration, typeDeclaration))); + }, + "AddBaseType"), + diagnostic); } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD114AvoidReturningNullTaskCodeFix.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD114AvoidReturningNullTaskCodeFix.cs index 261d792e8..290a1d838 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD114AvoidReturningNullTaskCodeFix.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD114AvoidReturningNullTaskCodeFix.cs @@ -1,72 +1,71 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using System.Collections.Immutable; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CodeActions; - using Microsoft.CodeAnalysis.CodeFixes; - using Microsoft.CodeAnalysis.Editing; - using Microsoft.CodeAnalysis.Operations; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Operations; - [ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic)] - public class VSTHRD114AvoidReturningNullTaskCodeFix : CodeFixProvider - { - private static readonly ImmutableArray ReusableFixableDiagnosticIds = ImmutableArray.Create( - AbstractVSTHRD114AvoidReturningNullTaskAnalyzer.Id); +namespace Microsoft.VisualStudio.Threading.Analyzers; + +[ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic)] +public class VSTHRD114AvoidReturningNullTaskCodeFix : CodeFixProvider +{ + private static readonly ImmutableArray ReusableFixableDiagnosticIds = ImmutableArray.Create( + AbstractVSTHRD114AvoidReturningNullTaskAnalyzer.Id); - /// - public override ImmutableArray FixableDiagnosticIds => ReusableFixableDiagnosticIds; + /// + public override ImmutableArray FixableDiagnosticIds => ReusableFixableDiagnosticIds; - /// - public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + /// + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; - public override async Task RegisterCodeFixesAsync(CodeFixContext context) + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + foreach (Diagnostic? diagnostic in context.Diagnostics) { - foreach (Diagnostic? diagnostic in context.Diagnostics) + SemanticModel? semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + SyntaxNode syntaxRoot = await context.Document.GetSyntaxRootOrThrowAsync(context.CancellationToken).ConfigureAwait(false); + SyntaxNode? nullLiteral = syntaxRoot.FindNode(diagnostic.Location.SourceSpan); + if (semanticModel?.GetOperation(nullLiteral, context.CancellationToken) is ILiteralOperation { ConstantValue: { HasValue: true, Value: null } }) { - SemanticModel? semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); - SyntaxNode? syntaxRoot = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - SyntaxNode? nullLiteral = syntaxRoot.FindNode(diagnostic.Location.SourceSpan); - if (semanticModel.GetOperation(nullLiteral, context.CancellationToken) is ILiteralOperation { ConstantValue: { HasValue: true, Value: null } }) + TypeInfo typeInfo = semanticModel.GetTypeInfo(nullLiteral, context.CancellationToken); + if (typeInfo.ConvertedType is INamedTypeSymbol returnType) { - TypeInfo typeInfo = semanticModel.GetTypeInfo(nullLiteral, context.CancellationToken); - if (typeInfo.ConvertedType is INamedTypeSymbol returnType) + if (returnType.IsGenericType) { - if (returnType.IsGenericType) - { - context.RegisterCodeFix(CodeAction.Create(Strings.VSTHRD114_CodeFix_FromResult, ct => ApplyTaskFromResultFix(returnType), nameof(Task.FromResult)), diagnostic); - } - else - { - context.RegisterCodeFix(CodeAction.Create(Strings.VSTHRD114_CodeFix_CompletedTask, ct => ApplyTaskCompletedTaskFix(returnType), nameof(Task.CompletedTask)), diagnostic); - } + context.RegisterCodeFix(CodeAction.Create(Strings.VSTHRD114_CodeFix_FromResult, ct => ApplyTaskFromResultFix(returnType), nameof(Task.FromResult)), diagnostic); } - - Task ApplyTaskCompletedTaskFix(INamedTypeSymbol returnType) + else { - var generator = SyntaxGenerator.GetGenerator(context.Document); - SyntaxNode completedTaskExpression = generator.MemberAccessExpression( - generator.TypeExpressionForStaticMemberAccess(returnType), - generator.IdentifierName(nameof(Task.CompletedTask))); - - return Task.FromResult(context.Document.WithSyntaxRoot(syntaxRoot.ReplaceNode(nullLiteral, completedTaskExpression))); + context.RegisterCodeFix(CodeAction.Create(Strings.VSTHRD114_CodeFix_CompletedTask, ct => ApplyTaskCompletedTaskFix(returnType), nameof(Task.CompletedTask)), diagnostic); } + } - Task ApplyTaskFromResultFix(INamedTypeSymbol returnType) - { - var generator = SyntaxGenerator.GetGenerator(context.Document); - SyntaxNode taskFromResultExpression = generator.InvocationExpression( - generator.MemberAccessExpression( - generator.TypeExpressionForStaticMemberAccess(returnType.BaseType), - generator.GenericName(nameof(Task.FromResult), returnType.TypeArguments[0])), - generator.NullLiteralExpression()); + Task ApplyTaskCompletedTaskFix(INamedTypeSymbol returnType) + { + var generator = SyntaxGenerator.GetGenerator(context.Document); + SyntaxNode completedTaskExpression = generator.MemberAccessExpression( + generator.TypeExpressionForStaticMemberAccess(returnType), + generator.IdentifierName(nameof(Task.CompletedTask))); - return Task.FromResult(context.Document.WithSyntaxRoot(syntaxRoot.ReplaceNode(nullLiteral, taskFromResultExpression))); - } + return Task.FromResult(context.Document.WithSyntaxRoot(syntaxRoot.ReplaceNode(nullLiteral, completedTaskExpression))); + } + + Task ApplyTaskFromResultFix(INamedTypeSymbol returnType) + { + var generator = SyntaxGenerator.GetGenerator(context.Document); + SyntaxNode taskFromResultExpression = generator.InvocationExpression( + generator.MemberAccessExpression( + generator.TypeExpressionForStaticMemberAccess(returnType.BaseType!), + generator.GenericName(nameof(Task.FromResult), returnType.TypeArguments[0])), + generator.NullLiteralExpression()); + + return Task.FromResult(context.Document.WithSyntaxRoot(syntaxRoot.ReplaceNode(nullLiteral, taskFromResultExpression))); } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD115AvoidJoinableTaskContextCtorWithNullArgsCodeFix.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD115AvoidJoinableTaskContextCtorWithNullArgsCodeFix.cs new file mode 100644 index 000000000..dbbd61137 --- /dev/null +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD115AvoidJoinableTaskContextCtorWithNullArgsCodeFix.cs @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Editing; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +[ExportCodeFixProvider(LanguageNames.CSharp)] +public class VSTHRD115AvoidJoinableTaskContextCtorWithNullArgsCodeFix : CodeFixProvider +{ + public const string SuppressWarningEquivalenceKey = "SuppressWarning"; + + public const string UseFactoryMethodEquivalenceKey = "UseFactoryMethod"; + + private static readonly ImmutableArray ReusableFixableDiagnosticIds = ImmutableArray.Create( + VSTHRD115AvoidJoinableTaskContextCtorWithNullArgsAnalyzer.Id); + + /// + public override ImmutableArray FixableDiagnosticIds => ReusableFixableDiagnosticIds; + + /// + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + foreach (Diagnostic diagnostic in context.Diagnostics) + { + SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken); + if (root is null) + { + continue; + } + + if (!diagnostic.Properties.TryGetValue(VSTHRD115AvoidJoinableTaskContextCtorWithNullArgsAnalyzer.NodeTypePropertyName, out string? nodeType) || nodeType is null) + { + continue; + } + + context.RegisterCodeFix(CodeAction.Create(Strings.VSTHRD115_CodeFix_Suppress_Title, ct => this.SuppressDiagnostic(context, root, nodeType, diagnostic, ct), SuppressWarningEquivalenceKey), diagnostic); + + if (diagnostic.Properties.TryGetValue(VSTHRD115AvoidJoinableTaskContextCtorWithNullArgsAnalyzer.UsesDefaultThreadPropertyName, out string? usesDefaultThreadString) && usesDefaultThreadString is "true") + { + context.RegisterCodeFix(CodeAction.Create(Strings.VSTHRD115_CodeFix_UseFactory_Title, ct => this.SwitchToFactory(context, root, nodeType, diagnostic, ct), UseFactoryMethodEquivalenceKey), diagnostic); + } + } + } + + private async Task SuppressDiagnostic(CodeFixContext context, SyntaxNode root, string nodeType, Diagnostic diagnostic, CancellationToken cancellationToken) + { + SyntaxGenerator generator = SyntaxGenerator.GetGenerator(context.Document); + SyntaxNode targetNode = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + + Compilation? compilation = await context.Document.Project.GetCompilationAsync(cancellationToken); + if (compilation is null) + { + return context.Document; + } + + ITypeSymbol? syncContext = compilation.GetTypeByMetadataName("System.Threading.SynchronizationContext"); + if (syncContext is null) + { + return context.Document; + } + + ITypeSymbol? jtc = compilation.GetTypeByMetadataName(Types.JoinableTaskContext.FullName); + if (jtc is null) + { + return context.Document; + } + + SyntaxNode syncContextCurrent = generator.MemberAccessExpression(generator.TypeExpression(syncContext, addImport: true), nameof(SynchronizationContext.Current)); + switch (nodeType) + { + case VSTHRD115AvoidJoinableTaskContextCtorWithNullArgsAnalyzer.NodeTypeArgument: + root = root.ReplaceNode(targetNode, syncContextCurrent); + break; + case VSTHRD115AvoidJoinableTaskContextCtorWithNullArgsAnalyzer.NodeTypeCreation: + (SyntaxNode? creationNode, SyntaxNode[]? args) = FixUtils.FindObjectCreationSyntax(targetNode); + if (creationNode is null || args is null) + { + return context.Document; + } + + SyntaxNode threadArg = args.Length >= 1 ? args[0] : generator.Argument(generator.NullLiteralExpression()); + SyntaxNode syncContextArg = generator.Argument(syncContextCurrent); + + root = root.ReplaceNode(creationNode, generator.ObjectCreationExpression(jtc, threadArg, syncContextArg)); + break; + } + + Document modifiedDocument = context.Document.WithSyntaxRoot(root); + return modifiedDocument; + } + + private async Task SwitchToFactory(CodeFixContext context, SyntaxNode root, string nodeType, Diagnostic diagnostic, CancellationToken cancellationToken) + { + SyntaxGenerator generator = SyntaxGenerator.GetGenerator(context.Document); + SyntaxNode targetNode = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + + Compilation? compilation = await context.Document.Project.GetCompilationAsync(cancellationToken); + if (compilation is null) + { + return context.Document; + } + + (SyntaxNode? creationExpression, _) = FixUtils.FindObjectCreationSyntax(targetNode); + if (creationExpression is null) + { + return context.Document; + } + + ITypeSymbol? jtc = compilation.GetTypeByMetadataName(Types.JoinableTaskContext.FullName); + if (jtc is null) + { + return context.Document; + } + + SyntaxNode factoryExpression = generator.InvocationExpression(generator.MemberAccessExpression(generator.TypeExpression(jtc, addImport: true), Types.JoinableTaskContext.CreateNoOpContext)); + + root = root.ReplaceNode(creationExpression, factoryExpression); + + Document modifiedDocument = context.Document.WithSyntaxRoot(root); + return modifiedDocument; + } +} diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD200UseAsyncNamingConventionCodeFix.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD200UseAsyncNamingConventionCodeFix.cs index 39fe62fab..e42db7232 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD200UseAsyncNamingConventionCodeFix.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD200UseAsyncNamingConventionCodeFix.cs @@ -1,82 +1,80 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Globalization; - using System.Linq; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CodeActions; - using Microsoft.CodeAnalysis.CodeFixes; - using Microsoft.CodeAnalysis.CSharp; - using Microsoft.CodeAnalysis.CSharp.Syntax; - using Microsoft.CodeAnalysis.Rename; +using System; +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Rename; - [ExportCodeFixProvider(LanguageNames.CSharp)] - public class VSTHRD200UseAsyncNamingConventionCodeFix : CodeFixProvider - { - private static readonly ImmutableArray ReusableFixableDiagnosticIds = ImmutableArray.Create( - VSTHRD200UseAsyncNamingConventionAnalyzer.Id); +namespace Microsoft.VisualStudio.Threading.Analyzers; - /// - public override ImmutableArray FixableDiagnosticIds => ReusableFixableDiagnosticIds; +[ExportCodeFixProvider(LanguageNames.CSharp)] +public class VSTHRD200UseAsyncNamingConventionCodeFix : CodeFixProvider +{ + private static readonly ImmutableArray ReusableFixableDiagnosticIds = ImmutableArray.Create( + VSTHRD200UseAsyncNamingConventionAnalyzer.Id); - /// - public override Task RegisterCodeFixesAsync(CodeFixContext context) + /// + public override ImmutableArray FixableDiagnosticIds => ReusableFixableDiagnosticIds; + + /// + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + Diagnostic diagnostic = context.Diagnostics.First(); + string? newName = diagnostic.Properties[VSTHRD200UseAsyncNamingConventionAnalyzer.NewNameKey]; + if (newName is not null) { - Diagnostic? diagnostic = context.Diagnostics.First(); - context.RegisterCodeFix(new AddAsyncSuffixCodeAction(context.Document, diagnostic), diagnostic); - return Task.FromResult(null); + context.RegisterCodeFix(new AddAsyncSuffixCodeAction(context.Document, diagnostic, newName), diagnostic); } - /// - public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + return Task.FromResult(null); + } - private class AddAsyncSuffixCodeAction : CodeAction - { - private readonly Diagnostic diagnostic; - private readonly Document document; + /// + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; - public AddAsyncSuffixCodeAction(Document document, Diagnostic diagnostic) - { - this.document = document; - this.diagnostic = diagnostic; - } + private class AddAsyncSuffixCodeAction : CodeAction + { + private readonly Diagnostic diagnostic; + private readonly Document document; + private readonly string newName; - public override string Title => string.Format( - CultureInfo.CurrentCulture, - Strings.VSTHRD200_CodeFix_Title, - this.NewName); + public AddAsyncSuffixCodeAction(Document document, Diagnostic diagnostic, string newName) + { + this.document = document; + this.diagnostic = diagnostic; + this.newName = newName; + } - /// - public override string? EquivalenceKey => null; + public override string Title => new LocalizableResourceString(nameof(Strings.VSTHRD200_CodeFix_Title), Strings.ResourceManager, typeof(Strings), this.newName).ToString(); - private string NewName => this.diagnostic.Properties[VSTHRD200UseAsyncNamingConventionAnalyzer.NewNameKey]; + /// + public override string? EquivalenceKey => null; - protected override async Task GetChangedSolutionAsync(CancellationToken cancellationToken) - { - SyntaxNode? root = await this.document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var methodDeclaration = (MethodDeclarationSyntax)root.FindNode(this.diagnostic.Location.SourceSpan); + protected override async Task GetChangedSolutionAsync(CancellationToken cancellationToken) + { + SyntaxNode root = await this.document.GetSyntaxRootOrThrowAsync(cancellationToken).ConfigureAwait(false); + SyntaxNode declaration = root.FindNode(this.diagnostic.Location.SourceSpan); - SemanticModel? semanticModel = await this.document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - IMethodSymbol? methodSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, cancellationToken); + SemanticModel? semanticModel = await this.document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + Solution? solution = this.document.Project.Solution; + IMethodSymbol? methodSymbol = semanticModel?.GetDeclaredSymbol(declaration, cancellationToken) as IMethodSymbol ?? throw new InvalidOperationException("Unable to get method symbol."); - Solution? solution = this.document.Project.Solution; - Solution? updatedSolution = await Renamer.RenameSymbolAsync( - solution, - methodSymbol, - this.NewName, - solution.Workspace.Options, - cancellationToken).ConfigureAwait(false); + Solution? updatedSolution = await Renamer.RenameSymbolAsync( + solution, + methodSymbol, + default(SymbolRenameOptions), + this.newName, + cancellationToken).ConfigureAwait(false); - return updatedSolution; - } + return updatedSolution; } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/build/AdditionalFiles/vs-threading.LegacyThreadSwitchingMembers.txt b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/buildTransitive/AdditionalFiles/vs-threading.LegacyThreadSwitchingMembers.txt similarity index 100% rename from src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/build/AdditionalFiles/vs-threading.LegacyThreadSwitchingMembers.txt rename to src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/buildTransitive/AdditionalFiles/vs-threading.LegacyThreadSwitchingMembers.txt diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/build/AdditionalFiles/vs-threading.MainThreadAssertingMethods.txt b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/buildTransitive/AdditionalFiles/vs-threading.MainThreadAssertingMethods.txt similarity index 100% rename from src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/build/AdditionalFiles/vs-threading.MainThreadAssertingMethods.txt rename to src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/buildTransitive/AdditionalFiles/vs-threading.MainThreadAssertingMethods.txt diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/build/AdditionalFiles/vs-threading.MainThreadSwitchingMethods.txt b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/buildTransitive/AdditionalFiles/vs-threading.MainThreadSwitchingMethods.txt similarity index 100% rename from src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/build/AdditionalFiles/vs-threading.MainThreadSwitchingMethods.txt rename to src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/buildTransitive/AdditionalFiles/vs-threading.MainThreadSwitchingMethods.txt diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/buildTransitive/AdditionalFiles/vs-threading.MembersRequiringMainThread.txt b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/buildTransitive/AdditionalFiles/vs-threading.MembersRequiringMainThread.txt new file mode 100644 index 000000000..e69de29bb diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/build/Microsoft.VisualStudio.Threading.Analyzers.targets b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/buildTransitive/Microsoft.VisualStudio.Threading.Analyzers.targets similarity index 100% rename from src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/build/Microsoft.VisualStudio.Threading.Analyzers.targets rename to src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/buildTransitive/Microsoft.VisualStudio.Threading.Analyzers.targets diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/AssemblyInfo.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/AssemblyInfo.cs index 18c0aa280..61f8ea2f6 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/AssemblyInfo.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/AssemblyInfo.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -10,6 +10,3 @@ [assembly: ComVisible(false)] [assembly: NeutralResourcesLanguage("en-US")] - -[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Threading.Analyzers.CodeFixes, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] -[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Threading.Analyzers.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic.csproj b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic.csproj index 2a9b5b675..110f7c1a4 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic.csproj +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic.csproj @@ -1,13 +1,16 @@  + - netstandard1.3 + netstandard2.0 Microsoft.VisualStudio.Threading.Analyzers + true + false false - - + + diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicUtils.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicUtils.cs index 81a12b77b..a8e74de1c 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicUtils.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicUtils.cs @@ -1,97 +1,99 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.CodeAnalysis.VisualBasic; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +internal sealed class VisualBasicUtils : LanguageUtils { - using System.Linq; - using System.Threading; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Operations; - using Microsoft.CodeAnalysis.VisualBasic.Syntax; + public static readonly VisualBasicUtils Instance = new VisualBasicUtils(); - internal sealed class VisualBasicUtils : LanguageUtils + private VisualBasicUtils() { - public static readonly VisualBasicUtils Instance = new VisualBasicUtils(); - - private VisualBasicUtils() - { - } + } - internal override Location? GetLocationOfBaseTypeName(INamedTypeSymbol symbol, INamedTypeSymbol baseType, Compilation compilation, CancellationToken cancellationToken) + public override Location? GetLocationOfBaseTypeName(INamedTypeSymbol symbol, INamedTypeSymbol baseType, Compilation compilation, CancellationToken cancellationToken) + { + foreach (SyntaxReference? syntaxReference in symbol.DeclaringSyntaxReferences) { - foreach (SyntaxReference? syntaxReference in symbol.DeclaringSyntaxReferences) + SyntaxNode? syntaxNode = syntaxReference.GetSyntax(cancellationToken); + if (syntaxNode is InterfaceStatementSyntax { Parent: InterfaceBlockSyntax vbInterface }) { - SyntaxNode? syntaxNode = syntaxReference.GetSyntax(cancellationToken); - if (syntaxNode is InterfaceStatementSyntax { Parent: InterfaceBlockSyntax vbInterface }) + if (compilation.GetSemanticModel(vbInterface.SyntaxTree) is { } semanticModel) { - if (compilation.GetSemanticModel(vbInterface.SyntaxTree) is { } semanticModel) + foreach (InheritsStatementSyntax? inheritStatement in vbInterface.Inherits) { - foreach (InheritsStatementSyntax? inheritStatement in vbInterface.Inherits) + foreach (TypeSyntax? typeSyntax in inheritStatement.Types) { - foreach (TypeSyntax? typeSyntax in inheritStatement.Types) + SymbolInfo baseTypeSymbolInfo = semanticModel.GetSymbolInfo(typeSyntax, cancellationToken); + if (SymbolEqualityComparer.Default.Equals(baseTypeSymbolInfo.Symbol, baseType)) { - SymbolInfo baseTypeSymbolInfo = semanticModel.GetSymbolInfo(typeSyntax, cancellationToken); - if (Equals(baseTypeSymbolInfo.Symbol, baseType)) - { - return typeSyntax.GetLocation(); - } + return typeSyntax.GetLocation(); } } } } - else if (syntaxNode is ClassStatementSyntax { Parent: ClassBlockSyntax vbClass }) + } + else if (syntaxNode is ClassStatementSyntax { Parent: ClassBlockSyntax vbClass }) + { + if (compilation.GetSemanticModel(vbClass.SyntaxTree) is { } semanticModel) { - if (compilation.GetSemanticModel(vbClass.SyntaxTree) is { } semanticModel) + foreach (ImplementsStatementSyntax? implementStatement in vbClass.Implements) { - foreach (ImplementsStatementSyntax? implementStatement in vbClass.Implements) + foreach (TypeSyntax? typeSyntax in implementStatement.Types) { - foreach (TypeSyntax? typeSyntax in implementStatement.Types) + SymbolInfo baseTypeSymbolInfo = semanticModel.GetSymbolInfo(typeSyntax, cancellationToken); + if (SymbolEqualityComparer.Default.Equals(baseTypeSymbolInfo.Symbol, baseType)) { - SymbolInfo baseTypeSymbolInfo = semanticModel.GetSymbolInfo(typeSyntax, cancellationToken); - if (Equals(baseTypeSymbolInfo.Symbol, baseType)) - { - return typeSyntax.GetLocation(); - } + return typeSyntax.GetLocation(); } } } } - else if (syntaxNode is StructureStatementSyntax { Parent: StructureBlockSyntax vbStruct }) + } + else if (syntaxNode is StructureStatementSyntax { Parent: StructureBlockSyntax vbStruct }) + { + if (compilation.GetSemanticModel(vbStruct.SyntaxTree) is { } semanticModel) { - if (compilation.GetSemanticModel(vbStruct.SyntaxTree) is { } semanticModel) + foreach (ImplementsStatementSyntax? implementStatement in vbStruct.Implements) { - foreach (ImplementsStatementSyntax? implementStatement in vbStruct.Implements) + foreach (TypeSyntax? typeSyntax in implementStatement.Types) { - foreach (TypeSyntax? typeSyntax in implementStatement.Types) + SymbolInfo baseTypeSymbolInfo = semanticModel.GetSymbolInfo(typeSyntax, cancellationToken); + if (SymbolEqualityComparer.Default.Equals(baseTypeSymbolInfo.Symbol, baseType)) { - SymbolInfo baseTypeSymbolInfo = semanticModel.GetSymbolInfo(typeSyntax, cancellationToken); - if (Equals(baseTypeSymbolInfo.Symbol, baseType)) - { - return typeSyntax.GetLocation(); - } + return typeSyntax.GetLocation(); } } } } } - - return symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(cancellationToken)?.GetLocation(); } - internal override SyntaxNode IsolateMethodName(IInvocationOperation invocation) - { - return invocation.Syntax; - } + return symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(cancellationToken)?.GetLocation(); + } - internal override SyntaxNode IsolateMethodName(IObjectCreationOperation objectCreation) - { - return objectCreation.Syntax; - } + public override SyntaxNode IsolateMethodName(IInvocationOperation invocation) + { + return invocation.Syntax; + } - internal override bool MethodReturnsNullableReferenceType(IMethodSymbol methodSymbol) - { - // VB.NET doesn't support nullable reference types - return false; - } + public override SyntaxNode IsolateMethodName(IObjectCreationOperation objectCreation) + { + return objectCreation.Syntax; + } + + public override bool MethodReturnsNullableReferenceType(IMethodSymbol methodSymbol) + { + // VB.NET doesn't support nullable reference types + return false; } + + public override bool IsAsyncMethod(SyntaxNode syntaxNode) => syntaxNode is MethodBlockSyntax methodDeclaration && methodDeclaration.BlockStatement.Modifiers.Any(SyntaxKind.AsyncKeyword); } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD001UseSwitchToMainThreadAsyncAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD001UseSwitchToMainThreadAsyncAnalyzer.cs index b24011765..ce469c865 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD001UseSwitchToMainThreadAsyncAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD001UseSwitchToMainThreadAsyncAnalyzer.cs @@ -1,14 +1,13 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers; - [DiagnosticAnalyzer(LanguageNames.VisualBasic)] - public sealed class VisualBasicVSTHRD001UseSwitchToMainThreadAsyncAnalyzer : AbstractVSTHRD001UseSwitchToMainThreadAsyncAnalyzer - { - private protected override LanguageUtils LanguageUtils => VisualBasicUtils.Instance; - } +[DiagnosticAnalyzer(LanguageNames.VisualBasic)] +public sealed class VisualBasicVSTHRD001UseSwitchToMainThreadAsyncAnalyzer : AbstractVSTHRD001UseSwitchToMainThreadAsyncAnalyzer +{ + protected override LanguageUtils LanguageUtils => VisualBasicUtils.Instance; } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD004AwaitSwitchToMainThreadAsyncAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD004AwaitSwitchToMainThreadAsyncAnalyzer.cs index cd7ef37c7..d55510ee6 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD004AwaitSwitchToMainThreadAsyncAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD004AwaitSwitchToMainThreadAsyncAnalyzer.cs @@ -1,14 +1,13 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers; - [DiagnosticAnalyzer(LanguageNames.VisualBasic)] - public sealed class VisualBasicVSTHRD004AwaitSwitchToMainThreadAsyncAnalyzer : AbstractVSTHRD004AwaitSwitchToMainThreadAsyncAnalyzer - { - private protected override LanguageUtils LanguageUtils => VisualBasicUtils.Instance; - } +[DiagnosticAnalyzer(LanguageNames.VisualBasic)] +public sealed class VisualBasicVSTHRD004AwaitSwitchToMainThreadAsyncAnalyzer : AbstractVSTHRD004AwaitSwitchToMainThreadAsyncAnalyzer +{ + protected override LanguageUtils LanguageUtils => VisualBasicUtils.Instance; } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD011UseAsyncLazyAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD011UseAsyncLazyAnalyzer.cs index 1ab6ea852..e6fe9f018 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD011UseAsyncLazyAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD011UseAsyncLazyAnalyzer.cs @@ -1,14 +1,13 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers; - [DiagnosticAnalyzer(LanguageNames.VisualBasic)] - public sealed class VisualBasicVSTHRD011UseAsyncLazyAnalyzer : AbstractVSTHRD011UseAsyncLazyAnalyzer - { - private protected override LanguageUtils LanguageUtils => VisualBasicUtils.Instance; - } +[DiagnosticAnalyzer(LanguageNames.VisualBasic)] +public sealed class VisualBasicVSTHRD011UseAsyncLazyAnalyzer : AbstractVSTHRD011UseAsyncLazyAnalyzer +{ + protected override LanguageUtils LanguageUtils => VisualBasicUtils.Instance; } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD012SpecifyJtfWhereAllowed.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD012SpecifyJtfWhereAllowed.cs index 61e7fcde4..39c3eb376 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD012SpecifyJtfWhereAllowed.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD012SpecifyJtfWhereAllowed.cs @@ -1,14 +1,13 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers; - [DiagnosticAnalyzer(LanguageNames.VisualBasic)] - public sealed class VisualBasicVSTHRD012SpecifyJtfWhereAllowed : AbstractVSTHRD012SpecifyJtfWhereAllowed - { - private protected override LanguageUtils LanguageUtils => VisualBasicUtils.Instance; - } +[DiagnosticAnalyzer(LanguageNames.VisualBasic)] +public sealed class VisualBasicVSTHRD012SpecifyJtfWhereAllowed : AbstractVSTHRD012SpecifyJtfWhereAllowed +{ + protected override LanguageUtils LanguageUtils => VisualBasicUtils.Instance; } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzer.cs index ff5d08517..a1414a949 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzer.cs @@ -1,14 +1,13 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers; - [DiagnosticAnalyzer(LanguageNames.VisualBasic)] - public sealed class VisualBasicVSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzer : AbstractVSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzer - { - private protected override LanguageUtils LanguageUtils => VisualBasicUtils.Instance; - } +[DiagnosticAnalyzer(LanguageNames.VisualBasic)] +public sealed class VisualBasicVSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzer : AbstractVSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzer +{ + protected override LanguageUtils LanguageUtils => VisualBasicUtils.Instance; } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD108AssertThreadRequirementUnconditionally.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD108AssertThreadRequirementUnconditionally.cs index 5b6c25c21..e25894b9c 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD108AssertThreadRequirementUnconditionally.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD108AssertThreadRequirementUnconditionally.cs @@ -1,14 +1,13 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers; - [DiagnosticAnalyzer(LanguageNames.VisualBasic)] - public sealed class VisualBasicVSTHRD108AssertThreadRequirementUnconditionally : AbstractVSTHRD108AssertThreadRequirementUnconditionally - { - private protected override LanguageUtils LanguageUtils => VisualBasicUtils.Instance; - } +[DiagnosticAnalyzer(LanguageNames.VisualBasic)] +public sealed class VisualBasicVSTHRD108AssertThreadRequirementUnconditionally : AbstractVSTHRD108AssertThreadRequirementUnconditionally +{ + protected override LanguageUtils LanguageUtils => VisualBasicUtils.Instance; } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD109AvoidAssertInAsyncMethodsAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD109AvoidAssertInAsyncMethodsAnalyzer.cs index 0fe805f70..f414b4107 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD109AvoidAssertInAsyncMethodsAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD109AvoidAssertInAsyncMethodsAnalyzer.cs @@ -1,14 +1,13 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers; - [DiagnosticAnalyzer(LanguageNames.VisualBasic)] - public sealed class VisualBasicVSTHRD109AvoidAssertInAsyncMethodsAnalyzer : AbstractVSTHRD109AvoidAssertInAsyncMethodsAnalyzer - { - private protected override LanguageUtils LanguageUtils => VisualBasicUtils.Instance; - } +[DiagnosticAnalyzer(LanguageNames.VisualBasic)] +public sealed class VisualBasicVSTHRD109AvoidAssertInAsyncMethodsAnalyzer : AbstractVSTHRD109AvoidAssertInAsyncMethodsAnalyzer +{ + protected override LanguageUtils LanguageUtils => VisualBasicUtils.Instance; } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD110ObserveResultOfAsyncCallsAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD110ObserveResultOfAsyncCallsAnalyzer.cs new file mode 100644 index 000000000..2f1657c99 --- /dev/null +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD110ObserveResultOfAsyncCallsAnalyzer.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.VisualBasic)] +public sealed class VisualBasicVSTHRD110ObserveResultOfAsyncCallsAnalyzer : AbstractVSTHRD110ObserveResultOfAsyncCallsAnalyzer +{ + protected override LanguageUtils LanguageUtils => VisualBasicUtils.Instance; +} diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD112ImplementSystemIAsyncDisposableAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD112ImplementSystemIAsyncDisposableAnalyzer.cs index 950ef4b12..9ce654001 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD112ImplementSystemIAsyncDisposableAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD112ImplementSystemIAsyncDisposableAnalyzer.cs @@ -1,14 +1,13 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers; - [DiagnosticAnalyzer(LanguageNames.VisualBasic)] - public sealed class VisualBasicVSTHRD112ImplementSystemIAsyncDisposableAnalyzer : AbstractVSTHRD112ImplementSystemIAsyncDisposableAnalyzer - { - private protected override LanguageUtils LanguageUtils => VisualBasicUtils.Instance; - } +[DiagnosticAnalyzer(LanguageNames.VisualBasic)] +public sealed class VisualBasicVSTHRD112ImplementSystemIAsyncDisposableAnalyzer : AbstractVSTHRD112ImplementSystemIAsyncDisposableAnalyzer +{ + protected override LanguageUtils LanguageUtils => VisualBasicUtils.Instance; } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD114AvoidReturningNullTaskAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD114AvoidReturningNullTaskAnalyzer.cs index 2639b42ac..5433c7d79 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD114AvoidReturningNullTaskAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.VisualBasic/VisualBasicVSTHRD114AvoidReturningNullTaskAnalyzer.cs @@ -1,14 +1,13 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers; - [DiagnosticAnalyzer(LanguageNames.VisualBasic)] - public sealed class VisualBasicVSTHRD114AvoidReturningNullTaskAnalyzer : AbstractVSTHRD114AvoidReturningNullTaskAnalyzer - { - private protected override LanguageUtils LanguageUtils => VisualBasicUtils.Instance; - } +[DiagnosticAnalyzer(LanguageNames.VisualBasic)] +public sealed class VisualBasicVSTHRD114AvoidReturningNullTaskAnalyzer : AbstractVSTHRD114AvoidReturningNullTaskAnalyzer +{ + protected override LanguageUtils LanguageUtils => VisualBasicUtils.Instance; } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD001UseSwitchToMainThreadAsyncAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD001UseSwitchToMainThreadAsyncAnalyzer.cs index 0cc75327b..cd428fe85 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD001UseSwitchToMainThreadAsyncAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD001UseSwitchToMainThreadAsyncAnalyzer.cs @@ -1,72 +1,71 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +public abstract class AbstractVSTHRD001UseSwitchToMainThreadAsyncAnalyzer : DiagnosticAnalyzer { - using System.Collections.Immutable; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; - using Microsoft.CodeAnalysis.Operations; + public const string Id = "VSTHRD001"; - public abstract class AbstractVSTHRD001UseSwitchToMainThreadAsyncAnalyzer : DiagnosticAnalyzer - { - public const string Id = "VSTHRD001"; + internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD001_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD001_MessageFormat), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); - internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD001_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD001_MessageFormat), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); + protected abstract LanguageUtils LanguageUtils { get; } - private protected abstract LanguageUtils LanguageUtils { get; } + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); - public override void Initialize(AnalysisContext context) + context.RegisterCompilationStartAction(compilationStartContext => { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + var legacyThreadSwitchingMembers = CommonInterest.ReadMethods(compilationStartContext.Options, CommonInterest.FileNamePatternForLegacyThreadSwitchingMembers, compilationStartContext.CancellationToken).ToImmutableArray(); + var analyzer = new Analyzer(this.LanguageUtils, legacyThreadSwitchingMembers); + compilationStartContext.RegisterOperationAction(Utils.DebuggableWrapper(analyzer.AnalyzeInvocation), OperationKind.Invocation); + }); + } - context.RegisterCompilationStartAction(compilationStartContext => - { - var legacyThreadSwitchingMembers = CommonInterest.ReadMethods(compilationStartContext.Options, CommonInterest.FileNamePatternForLegacyThreadSwitchingMembers, compilationStartContext.CancellationToken).ToImmutableArray(); - var analyzer = new Analyzer(this.LanguageUtils, legacyThreadSwitchingMembers); - compilationStartContext.RegisterOperationAction(Utils.DebuggableWrapper(analyzer.AnalyzeInvocation), OperationKind.Invocation); - }); - } + private class Analyzer + { + private readonly LanguageUtils languageUtils; + private readonly ImmutableArray legacyThreadSwitchingMembers; - private class Analyzer + internal Analyzer(LanguageUtils languageUtils, ImmutableArray legacyThreadSwitchingMembers) { - private readonly LanguageUtils languageUtils; - private readonly ImmutableArray legacyThreadSwitchingMembers; - - internal Analyzer(LanguageUtils languageUtils, ImmutableArray legacyThreadSwitchingMembers) - { - this.languageUtils = languageUtils; - this.legacyThreadSwitchingMembers = legacyThreadSwitchingMembers; - } + this.languageUtils = languageUtils; + this.legacyThreadSwitchingMembers = legacyThreadSwitchingMembers; + } - internal void AnalyzeInvocation(OperationAnalysisContext context) + internal void AnalyzeInvocation(OperationAnalysisContext context) + { + var invocation = (IInvocationOperation)context.Operation; + IMethodSymbol? invokeMethod = invocation.TargetMethod; + if (invokeMethod is object) { - var invocation = (IInvocationOperation)context.Operation; - IMethodSymbol? invokeMethod = invocation.TargetMethod; - if (invokeMethod is object) + foreach (CommonInterest.QualifiedMember legacyMethod in this.legacyThreadSwitchingMembers) { - foreach (CommonInterest.QualifiedMember legacyMethod in this.legacyThreadSwitchingMembers) - { - context.CancellationToken.ThrowIfCancellationRequested(); + context.CancellationToken.ThrowIfCancellationRequested(); - if (legacyMethod.IsMatch(invokeMethod)) - { - var diagnostic = Diagnostic.Create( - Descriptor, - this.languageUtils.IsolateMethodName(invocation).GetLocation()); - context.ReportDiagnostic(diagnostic); - break; - } + if (legacyMethod.IsMatch(invokeMethod)) + { + var diagnostic = Diagnostic.Create( + Descriptor, + this.languageUtils.IsolateMethodName(invocation).GetLocation()); + context.ReportDiagnostic(diagnostic); + break; } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD004AwaitSwitchToMainThreadAsyncAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD004AwaitSwitchToMainThreadAsyncAnalyzer.cs index 85c58bed1..b9a34c939 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD004AwaitSwitchToMainThreadAsyncAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD004AwaitSwitchToMainThreadAsyncAnalyzer.cs @@ -1,57 +1,67 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +/// +/// Detects invocations of JoinableTaskFactory.SwitchToMainThreadAsync that are not awaited. +/// +public abstract class AbstractVSTHRD004AwaitSwitchToMainThreadAsyncAnalyzer : DiagnosticAnalyzer { - using System.Collections.Immutable; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; - using Microsoft.CodeAnalysis.Operations; - - /// - /// Detects invocations of JoinableTaskFactory.SwitchToMainThreadAsync that are not awaited. - /// - public abstract class AbstractVSTHRD004AwaitSwitchToMainThreadAsyncAnalyzer : DiagnosticAnalyzer - { - public const string Id = "VSTHRD004"; + public const string Id = "VSTHRD004"; - internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD004_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD004_MessageFormat), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); + internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD004_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD004_MessageFormat), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); - /// - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); - private protected abstract LanguageUtils LanguageUtils { get; } + protected abstract LanguageUtils LanguageUtils { get; } - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); - context.RegisterOperationAction(Utils.DebuggableWrapper(this.AnalyzeInvocation), OperationKind.Invocation); - } + context.RegisterOperationAction(Utils.DebuggableWrapper(this.AnalyzeInvocation), OperationKind.Invocation); + } - private void AnalyzeInvocation(OperationAnalysisContext context) + private void AnalyzeInvocation(OperationAnalysisContext context) + { + var invocation = (IInvocationOperation)context.Operation; + IMethodSymbol? methodSymbol = invocation.TargetMethod; + if (methodSymbol.Name == Types.JoinableTaskFactory.SwitchToMainThreadAsync && + methodSymbol.ContainingType.Name == Types.JoinableTaskFactory.TypeName && + methodSymbol.ContainingType.BelongsToNamespace(Types.JoinableTaskFactory.Namespace)) { - var invocation = (IInvocationOperation)context.Operation; - IMethodSymbol? methodSymbol = invocation.TargetMethod; - if (methodSymbol.Name == Types.JoinableTaskFactory.SwitchToMainThreadAsync && - methodSymbol.ContainingType.Name == Types.JoinableTaskFactory.TypeName && - methodSymbol.ContainingType.BelongsToNamespace(Types.JoinableTaskFactory.Namespace)) + // This is a call to JTF.SwitchToMainThreadAsync(). Is it being awaited in some ancestor? + for (IOperation? parentOp = invocation.Parent; parentOp is not null; parentOp = parentOp.Parent) { - // This is a call to JTF.SwitchToMainThreadAsync(). Is it being (directly) awaited? - if (!(invocation.Parent is IAwaitOperation)) + if (parentOp is IAwaitOperation) + { + return; + } + + if (parentOp is IExpressionStatementOperation or IReturnOperation) { - Location? location = (this.LanguageUtils.IsolateMethodName(invocation) ?? invocation.Syntax).GetLocation(); - context.ReportDiagnostic(Diagnostic.Create(Descriptor, location)); + // We've reached the top of the statement without finding an await. + break; } } + + Location? location = (this.LanguageUtils.IsolateMethodName(invocation) ?? invocation.Syntax).GetLocation(); + context.ReportDiagnostic(Diagnostic.Create(Descriptor, location)); } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD011UseAsyncLazyAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD011UseAsyncLazyAnalyzer.cs index adf01b2c7..705578acd 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD011UseAsyncLazyAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD011UseAsyncLazyAnalyzer.cs @@ -1,84 +1,83 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using System.Collections.Immutable; - using System.Linq; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; - using Microsoft.CodeAnalysis.Operations; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; - public abstract class AbstractVSTHRD011UseAsyncLazyAnalyzer : DiagnosticAnalyzer - { - public const string Id = "VSTHRD011"; +namespace Microsoft.VisualStudio.Threading.Analyzers; - internal static readonly DiagnosticDescriptor LazyOfTaskDescriptor = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD011_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD011_MessageFormat), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); +public abstract class AbstractVSTHRD011UseAsyncLazyAnalyzer : DiagnosticAnalyzer +{ + public const string Id = "VSTHRD011"; - internal static readonly DiagnosticDescriptor SyncBlockInValueFactoryDescriptor = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD011_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD011b_MessageFormat), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); + public static readonly DiagnosticDescriptor LazyOfTaskDescriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD011_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD011_MessageFormat), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); - /// - public override ImmutableArray SupportedDiagnostics - { - get { return ImmutableArray.Create(LazyOfTaskDescriptor, SyncBlockInValueFactoryDescriptor); } - } + public static readonly DiagnosticDescriptor SyncBlockInValueFactoryDescriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD011_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD011b_MessageFormat), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); - private protected abstract LanguageUtils LanguageUtils { get; } + /// + public override ImmutableArray SupportedDiagnostics + { + get { return ImmutableArray.Create(LazyOfTaskDescriptor, SyncBlockInValueFactoryDescriptor); } + } - /// - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + protected abstract LanguageUtils LanguageUtils { get; } - context.RegisterOperationAction( - Utils.DebuggableWrapper(this.AnalyzeNode), - OperationKind.ObjectCreation); - } + /// + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + + context.RegisterOperationAction( + Utils.DebuggableWrapper(this.AnalyzeNode), + OperationKind.ObjectCreation); + } - private void AnalyzeNode(OperationAnalysisContext context) + private void AnalyzeNode(OperationAnalysisContext context) + { + var objectCreation = (IObjectCreationOperation)context.Operation; + IMethodSymbol? methodSymbol = objectCreation.Constructor; + var constructedType = methodSymbol?.ReceiverType as INamedTypeSymbol; + if (Utils.IsLazyOfT(constructedType)) { - var objectCreation = (IObjectCreationOperation)context.Operation; - IMethodSymbol? methodSymbol = objectCreation.Constructor; - var constructedType = methodSymbol?.ReceiverType as INamedTypeSymbol; - if (Utils.IsLazyOfT(constructedType)) + ITypeSymbol? typeArg = constructedType.TypeArguments.FirstOrDefault(); + bool typeArgIsTask = typeArg?.Name == nameof(Task) + && typeArg.BelongsToNamespace(Namespaces.SystemThreadingTasks); + if (typeArgIsTask) { - ITypeSymbol? typeArg = constructedType.TypeArguments.FirstOrDefault(); - bool typeArgIsTask = typeArg?.Name == nameof(Task) - && typeArg.BelongsToNamespace(Namespaces.SystemThreadingTasks); - if (typeArgIsTask) - { - context.ReportDiagnostic(Diagnostic.Create(LazyOfTaskDescriptor, this.LanguageUtils.IsolateMethodName(objectCreation).GetLocation())); - } - else + context.ReportDiagnostic(Diagnostic.Create(LazyOfTaskDescriptor, this.LanguageUtils.IsolateMethodName(objectCreation).GetLocation())); + } + else + { + IOperation? firstArgExpression = objectCreation.Arguments.FirstOrDefault()?.Value; + if (firstArgExpression is IDelegateCreationOperation { Target: IAnonymousFunctionOperation anonFunc }) { - IOperation? firstArgExpression = objectCreation.Arguments.FirstOrDefault()?.Value; - if (firstArgExpression is IDelegateCreationOperation { Target: IAnonymousFunctionOperation anonFunc }) + System.Collections.Generic.IEnumerable? problems = from invocation in anonFunc.Descendants().OfType() + let invokedSymbol = invocation.TargetMethod + where invokedSymbol is object && CommonInterest.SyncBlockingMethods.Any(m => m.Method.IsMatch(invokedSymbol)) + select invocation; + IInvocationOperation? firstProblem = problems.FirstOrDefault(); + if (firstProblem is object) { - System.Collections.Generic.IEnumerable? problems = from invocation in anonFunc.Descendants().OfType() - let invokedSymbol = invocation.TargetMethod - where invokedSymbol is object && CommonInterest.SyncBlockingMethods.Any(m => m.Method.IsMatch(invokedSymbol)) - select invocation; - IInvocationOperation? firstProblem = problems.FirstOrDefault(); - if (firstProblem is object) - { - context.ReportDiagnostic(Diagnostic.Create(SyncBlockInValueFactoryDescriptor, this.LanguageUtils.IsolateMethodName(firstProblem).GetLocation())); - } + context.ReportDiagnostic(Diagnostic.Create(SyncBlockInValueFactoryDescriptor, this.LanguageUtils.IsolateMethodName(firstProblem).GetLocation())); } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD012SpecifyJtfWhereAllowed.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD012SpecifyJtfWhereAllowed.cs index 3ad070f19..06c8071d4 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD012SpecifyJtfWhereAllowed.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD012SpecifyJtfWhereAllowed.cs @@ -1,89 +1,73 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +public abstract class AbstractVSTHRD012SpecifyJtfWhereAllowed : DiagnosticAnalyzer { - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Linq; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; - using Microsoft.CodeAnalysis.Operations; - - public abstract class AbstractVSTHRD012SpecifyJtfWhereAllowed : DiagnosticAnalyzer - { - public const string Id = "VSTHRD012"; + public const string Id = "VSTHRD012"; - internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD012_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD012_MessageFormat), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); + internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD012_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD012_MessageFormat), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); - private protected abstract LanguageUtils LanguageUtils { get; } + protected abstract LanguageUtils LanguageUtils { get; } - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); - context.RegisterOperationAction(Utils.DebuggableWrapper(this.AnalyzeInvocation), OperationKind.Invocation); - context.RegisterOperationAction(Utils.DebuggableWrapper(this.AnalyzerObjectCreation), OperationKind.ObjectCreation); - } + context.RegisterOperationAction(Utils.DebuggableWrapper(this.AnalyzeInvocation), OperationKind.Invocation); + context.RegisterOperationAction(Utils.DebuggableWrapper(this.AnalyzeObjectCreation), OperationKind.ObjectCreation); + } - private static bool IsImportantJtfParameter(IParameterSymbol ps) - { - return (ps.Type.Name == Types.JoinableTaskContext.TypeName - || ps.Type.Name == Types.JoinableTaskFactory.TypeName - || ps.Type.Name == Types.JoinableTaskCollection.TypeName) - && ps.Type.BelongsToNamespace(Namespaces.MicrosoftVisualStudioThreading) - && !ps.GetAttributes().Any(a => a.AttributeClass.Name == "OptionalAttribute"); - } + private static bool IsImportantJtfParameter(IParameterSymbol ps) + { + return (ps.Type.Name == Types.JoinableTaskContext.TypeName + || ps.Type.Name == Types.JoinableTaskFactory.TypeName + || ps.Type.Name == Types.JoinableTaskCollection.TypeName) + && ps.Type.BelongsToNamespace(Namespaces.MicrosoftVisualStudioThreading) + && !ps.GetAttributes().Any(a => a.AttributeClass?.Name == "OptionalAttribute"); + } - private static IArgumentOperation? GetArgumentForParameter(ImmutableArray arguments, IParameterSymbol parameter) + private static IArgumentOperation? GetArgumentForParameter(ImmutableArray arguments, IParameterSymbol parameter) + { + foreach (IArgumentOperation? argument in arguments) { - foreach (IArgumentOperation? argument in arguments) + if (SymbolEqualityComparer.Default.Equals(argument.Parameter, parameter)) { - if (Equals(argument.Parameter, parameter)) - { - return argument; - } + return argument; } - - return null; } - private static void AnalyzeCall(OperationAnalysisContext context, Location location, ImmutableArray argList, IMethodSymbol methodSymbol, IEnumerable otherOverloads) + return null; + } + + private static void AnalyzeCall(OperationAnalysisContext context, Location location, ImmutableArray argList, IMethodSymbol methodSymbol, IEnumerable otherOverloads) + { + IParameterSymbol? firstJtfParameter = methodSymbol.Parameters.FirstOrDefault(IsImportantJtfParameter); + if (firstJtfParameter is object) { - IParameterSymbol? firstJtfParameter = methodSymbol.Parameters.FirstOrDefault(IsImportantJtfParameter); - if (firstJtfParameter is object) - { - // Verify that if the JTF/JTC parameter is optional, it is actually specified in the caller's syntax. - if (firstJtfParameter.HasExplicitDefaultValue) - { - IArgumentOperation? argument = GetArgumentForParameter(argList, firstJtfParameter); - if (argument is null || argument.IsImplicit) - { - Diagnostic diagnostic = Diagnostic.Create( - Descriptor, - location); - context.ReportDiagnostic(diagnostic); - } - } - } - else + // Verify that if the JTF/JTC parameter is optional, it is actually specified in the caller's syntax. + if (firstJtfParameter.HasExplicitDefaultValue) { - // The method being invoked doesn't take any JTC/JTF parameters. - // Look for an overload that does. - bool preferableAlternativesExist = otherOverloads - .Where(m => !m.IsObsolete()) - .Any(m => m.Parameters.Skip(m.IsExtensionMethod ? 1 : 0).Any(IsImportantJtfParameter)); - if (preferableAlternativesExist) + IArgumentOperation? argument = GetArgumentForParameter(argList, firstJtfParameter); + if (argument is null || argument.IsImplicit) { Diagnostic diagnostic = Diagnostic.Create( Descriptor, @@ -92,28 +76,50 @@ private static void AnalyzeCall(OperationAnalysisContext context, Location locat } } } - - private void AnalyzeInvocation(OperationAnalysisContext context) + else { - var invocation = (IInvocationOperation)context.Operation; - SyntaxNode? invokedMethodName = this.LanguageUtils.IsolateMethodName(invocation); - ImmutableArray argList = invocation.Arguments; - IMethodSymbol? methodSymbol = invocation.TargetMethod; - - IEnumerable? otherOverloads = methodSymbol.ContainingType.GetMembers(methodSymbol.Name).OfType(); - AnalyzeCall(context, invokedMethodName.GetLocation(), argList, methodSymbol, otherOverloads); + // The method being invoked doesn't take any JTC/JTF parameters. + // Look for an overload that does. + bool preferableAlternativesExist = otherOverloads + .Any(m => + !m.IsObsolete() && + m.Parameters.Skip(m.IsExtensionMethod ? 1 : 0).Any(IsImportantJtfParameter) && + context.ContainingSymbol.FindContainingNamedOrAssemblySymbol() is ISymbol containingSymbol && context.Compilation.IsSymbolAccessibleWithin(m, containingSymbol)); + if (preferableAlternativesExist) + { + Diagnostic diagnostic = Diagnostic.Create( + Descriptor, + location); + context.ReportDiagnostic(diagnostic); + } } + } + + private void AnalyzeInvocation(OperationAnalysisContext context) + { + var invocation = (IInvocationOperation)context.Operation; + SyntaxNode? invokedMethodName = this.LanguageUtils.IsolateMethodName(invocation); + ImmutableArray argList = invocation.Arguments; + IMethodSymbol? methodSymbol = invocation.TargetMethod; - private void AnalyzerObjectCreation(OperationAnalysisContext context) + IEnumerable? otherOverloads = methodSymbol.ContainingType.GetMembers(methodSymbol.Name).OfType(); + AnalyzeCall(context, invokedMethodName.GetLocation(), argList, methodSymbol, otherOverloads); + } + + private void AnalyzeObjectCreation(OperationAnalysisContext context) + { + var objectCreation = (IObjectCreationOperation)context.Operation; + IMethodSymbol? methodSymbol = objectCreation.Constructor; + if (methodSymbol is null) { - var objectCreation = (IObjectCreationOperation)context.Operation; - IMethodSymbol? methodSymbol = objectCreation.Constructor; - AnalyzeCall( - context, - this.LanguageUtils.IsolateMethodName(objectCreation).GetLocation(), - objectCreation.Arguments, - methodSymbol, - methodSymbol.ContainingType.Constructors); + return; } + + AnalyzeCall( + context, + this.LanguageUtils.IsolateMethodName(objectCreation).GetLocation(), + objectCreation.Arguments, + methodSymbol, + methodSymbol.ContainingType.Constructors); } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzer.cs index c8e0fe351..df3e9845f 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzer.cs @@ -1,82 +1,81 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using System; - using System.Collections.Immutable; - using System.Linq; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; - using Microsoft.CodeAnalysis.Operations; +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; - /// - /// Report errors when or - /// overloads are invoked that do not accept an explicit . - /// - /// - /// Not specifying the explicitly is problematic because - /// will then be used. While this is normally (the thread pool), it may not always be. - /// For example, when the calling code is itself running as a scheduled task on a different , - /// then that will be inherited, leading to the calling code to run in an unexpected context. - /// Explicitly specifying will ensure that the behavior is always to run the - /// on the thread pool. Of course any is fine, so long as it is explicitly given (including - /// itself). - /// - public abstract class AbstractVSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzer : DiagnosticAnalyzer - { - public const string Id = "VSTHRD105"; +namespace Microsoft.VisualStudio.Threading.Analyzers; - internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD105_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD105_MessageFormat), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); +/// +/// Report errors when or +/// overloads are invoked that do not accept an explicit . +/// +/// +/// Not specifying the explicitly is problematic because +/// will then be used. While this is normally (the thread pool), it may not always be. +/// For example, when the calling code is itself running as a scheduled task on a different , +/// then that will be inherited, leading to the calling code to run in an unexpected context. +/// Explicitly specifying will ensure that the behavior is always to run the +/// on the thread pool. Of course any is fine, so long as it is explicitly given (including +/// itself). +/// +public abstract class AbstractVSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzer : DiagnosticAnalyzer +{ + public const string Id = "VSTHRD105"; - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); + internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD105_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD105_MessageFormat), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); - private protected abstract LanguageUtils LanguageUtils { get; } + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + protected abstract LanguageUtils LanguageUtils { get; } - context.RegisterOperationAction(Utils.DebuggableWrapper(this.AnalyzeInvocation), OperationKind.Invocation); - } + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + + context.RegisterOperationAction(Utils.DebuggableWrapper(this.AnalyzeInvocation), OperationKind.Invocation); + } - private void AnalyzeInvocation(OperationAnalysisContext context) + private void AnalyzeInvocation(OperationAnalysisContext context) + { + var operation = (IInvocationOperation)context.Operation; + IMethodSymbol? invokeMethod = operation.TargetMethod; + if (invokeMethod?.ContainingType.BelongsToNamespace(Namespaces.SystemThreadingTasks) ?? false) { - var operation = (IInvocationOperation)context.Operation; - IMethodSymbol? invokeMethod = operation.TargetMethod; - if (invokeMethod?.ContainingType.BelongsToNamespace(Namespaces.SystemThreadingTasks) ?? false) - { - bool reportDiagnostic = false; - bool isContinueWith = invokeMethod.Name == nameof(Task.ContinueWith) && invokeMethod.ContainingType.Name == nameof(Task); - bool isTaskFactoryStartNew = invokeMethod.Name == nameof(TaskFactory.StartNew) && invokeMethod.ContainingType.Name == nameof(TaskFactory); + bool reportDiagnostic = false; + bool isContinueWith = invokeMethod.Name == nameof(Task.ContinueWith) && invokeMethod.ContainingType.Name == nameof(Task); + bool isTaskFactoryStartNew = invokeMethod.Name == nameof(TaskFactory.StartNew) && invokeMethod.ContainingType.Name == nameof(TaskFactory); - if (isContinueWith || isTaskFactoryStartNew) + if (isContinueWith || isTaskFactoryStartNew) + { + if (!invokeMethod.Parameters.Any(p => p.Type.Name == nameof(TaskScheduler) && p.Type.BelongsToNamespace(Namespaces.SystemThreadingTasks))) { - if (!invokeMethod.Parameters.Any(p => p.Type.Name == nameof(TaskScheduler) && p.Type.BelongsToNamespace(Namespaces.SystemThreadingTasks))) - { - reportDiagnostic |= isContinueWith; + reportDiagnostic |= isContinueWith; - // Only notice uses of TaskFactory on the static instance (since custom instances may have a non-problematic default TaskScheduler set). - reportDiagnostic |= isTaskFactoryStartNew - && operation.Instance is IPropertyReferenceOperation { Property: { } factoryProperty } - && factoryProperty.ContainingType.Name == Types.Task.TypeName && factoryProperty.ContainingType.BelongsToNamespace(Namespaces.SystemThreadingTasks) - && factoryProperty.Name == nameof(Task.Factory); - } + // Only notice uses of TaskFactory on the static instance (since custom instances may have a non-problematic default TaskScheduler set). + reportDiagnostic |= isTaskFactoryStartNew + && operation.Instance is IPropertyReferenceOperation { Property: { } factoryProperty } + && factoryProperty.ContainingType.Name == Types.Task.TypeName && factoryProperty.ContainingType.BelongsToNamespace(Namespaces.SystemThreadingTasks) + && factoryProperty.Name == nameof(Task.Factory); } + } - if (reportDiagnostic) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptor, this.LanguageUtils.IsolateMethodName(operation).GetLocation())); - } + if (reportDiagnostic) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptor, this.LanguageUtils.IsolateMethodName(operation).GetLocation())); } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD108AssertThreadRequirementUnconditionally.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD108AssertThreadRequirementUnconditionally.cs index 30d41c47d..676bd4207 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD108AssertThreadRequirementUnconditionally.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD108AssertThreadRequirementUnconditionally.cs @@ -1,138 +1,137 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +/// +/// Report warnings when methods that assert their thread affinity only do so under a condition, +/// leaving its true thread requirements less discoverable to the caller. +/// +/// +/// When you have code like this: +/// +/// It's problematic because callers usually aren't prepared for thread affinity sometimes. +/// As a result, code that happens to not execute the conditional logic of the method will +/// temporarily get away with it but then get stung by it later. We want to front-load discovery +/// of such bugs as early as possible by making synchronous methods unconditionally thread affinitized. +/// So a new analyzer should require that the thread-asserting statement appear near the top of the method, and outside of any conditional blocks. +/// +public abstract class AbstractVSTHRD108AssertThreadRequirementUnconditionally : DiagnosticAnalyzer { - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Linq; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; - using Microsoft.CodeAnalysis.Operations; - - /// - /// Report warnings when methods that assert their thread affinity only do so under a condition, - /// leaving its true thread requirements less discoverable to the caller. - /// - /// - /// When you have code like this: - /// - /// It's problematic because callers usually aren't prepared for thread affinity sometimes. - /// As a result, code that happens to not execute the conditional logic of the method will - /// temporarily get away with it but then get stung by it later. We want to front-load discovery - /// of such bugs as early as possible by making synchronous methods unconditionally thread affinitized. - /// So a new analyzer should require that the thread-asserting statement appear near the top of the method, and outside of any conditional blocks. - /// - public abstract class AbstractVSTHRD108AssertThreadRequirementUnconditionally : DiagnosticAnalyzer - { - public const string Id = "VSTHRD108"; + public const string Id = "VSTHRD108"; - internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD108_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD108_MessageFormat), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); + internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD108_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD108_MessageFormat), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); - /// - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); - private protected abstract LanguageUtils LanguageUtils { get; } + protected abstract LanguageUtils LanguageUtils { get; } - /// - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + /// + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); - context.RegisterCompilationStartAction(ctxt => - { - var mainThreadAssertingMethods = CommonInterest.ReadMethods(ctxt.Options, CommonInterest.FileNamePatternForMethodsThatAssertMainThread, ctxt.CancellationToken).ToImmutableArray(); + context.RegisterCompilationStartAction(ctxt => + { + var mainThreadAssertingMethods = CommonInterest.ReadMethods(ctxt.Options, CommonInterest.FileNamePatternForMethodsThatAssertMainThread, ctxt.CancellationToken).ToImmutableArray(); - ctxt.RegisterOperationAction(Utils.DebuggableWrapper(c => this.AnalyzeInvocation(c, mainThreadAssertingMethods)), OperationKind.Invocation); - }); - } + ctxt.RegisterOperationAction(Utils.DebuggableWrapper(c => this.AnalyzeInvocation(c, mainThreadAssertingMethods)), OperationKind.Invocation); + }); + } - private static bool IsInConditional(IOperation operation) + private static bool IsInConditional(IOperation operation) + { + foreach (IOperation? ancestor in GetAncestorsWithinMethod(operation)) { - foreach (IOperation? ancestor in GetAncestorsWithinMethod(operation)) + if (ancestor is IConditionalOperation || + ancestor is IWhileLoopOperation { ConditionIsTop: true } || + ancestor is IForLoopOperation) { - if (ancestor is IConditionalOperation || - ancestor is IWhileLoopOperation { ConditionIsTop: true } || - ancestor is IForLoopOperation) - { - return true; - } + return true; } - - return false; } - private static IEnumerable GetAncestorsWithinMethod(IOperation operation) + return false; + } + + private static IEnumerable GetAncestorsWithinMethod(IOperation operation) + { + for (IOperation? current = operation; current is object; current = current.Parent) { - for (IOperation current = operation; current is object; current = current.Parent) + if (current is ILocalFunctionOperation || current is IAnonymousFunctionOperation) { - if (current is ILocalFunctionOperation || current is IAnonymousFunctionOperation) - { - yield break; - } - - yield return current; + yield break; } + + yield return current; } + } - private static bool IsArgInInvocationToConditionalMethod(OperationAnalysisContext context) + private static bool IsArgInInvocationToConditionalMethod(OperationAnalysisContext context) + { + IArgumentOperation? argument = GetAncestorsWithinMethod(context.Operation).OfType().FirstOrDefault(); + var containingInvocation = argument?.Parent as IInvocationOperation; + if (containingInvocation is object) { - IArgumentOperation? argument = GetAncestorsWithinMethod(context.Operation).OfType().FirstOrDefault(); - var containingInvocation = argument?.Parent as IInvocationOperation; - if (containingInvocation is object) - { - IMethodSymbol? symbolOfContainingMethodInvocation = containingInvocation.TargetMethod; - return symbolOfContainingMethodInvocation?.GetAttributes().Any(a => - a.AttributeClass.BelongsToNamespace(Namespaces.SystemDiagnostics) && - a.AttributeClass.Name == nameof(System.Diagnostics.ConditionalAttribute)) ?? false; - } - - return false; + IMethodSymbol? symbolOfContainingMethodInvocation = containingInvocation.TargetMethod; + return symbolOfContainingMethodInvocation?.GetAttributes().Any(a => + a.AttributeClass?.BelongsToNamespace(Namespaces.SystemDiagnostics) is true && + a.AttributeClass.Name == nameof(System.Diagnostics.ConditionalAttribute)) ?? false; } - private void AnalyzeInvocation(OperationAnalysisContext context, ImmutableArray mainThreadAssertingMethods) + return false; + } + + private void AnalyzeInvocation(OperationAnalysisContext context, ImmutableArray mainThreadAssertingMethods) + { + var invocation = (IInvocationOperation)context.Operation; + IMethodSymbol? symbol = invocation.TargetMethod; + if (symbol is object) { - var invocation = (IInvocationOperation)context.Operation; - IMethodSymbol? symbol = invocation.TargetMethod; - if (symbol is object) + bool reportDiagnostic = false; + if (mainThreadAssertingMethods.Contains(symbol)) { - bool reportDiagnostic = false; - if (mainThreadAssertingMethods.Contains(symbol)) + if (IsInConditional(invocation)) { - if (IsInConditional(invocation)) - { - reportDiagnostic = true; - } + reportDiagnostic = true; } - else if (CommonInterest.ThreadAffinityTestingMethods.Any(m => m.IsMatch(symbol))) + } + else if (CommonInterest.ThreadAffinityTestingMethods.Any(m => m.IsMatch(symbol))) + { + if (IsArgInInvocationToConditionalMethod(context)) { - if (IsArgInInvocationToConditionalMethod(context)) - { - reportDiagnostic = true; - } + reportDiagnostic = true; } + } - if (reportDiagnostic) - { - SyntaxNode? nodeToLocate = this.LanguageUtils.IsolateMethodName(invocation); - context.ReportDiagnostic(Diagnostic.Create(Descriptor, nodeToLocate.GetLocation())); - } + if (reportDiagnostic) + { + SyntaxNode? nodeToLocate = this.LanguageUtils.IsolateMethodName(invocation); + context.ReportDiagnostic(Diagnostic.Create(Descriptor, nodeToLocate.GetLocation())); } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD109AvoidAssertInAsyncMethodsAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD109AvoidAssertInAsyncMethodsAnalyzer.cs index 0aae792a7..f40739d99 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD109AvoidAssertInAsyncMethodsAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD109AvoidAssertInAsyncMethodsAnalyzer.cs @@ -1,71 +1,70 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +/// +/// Report errors when async methods throw when not on the main thread instead of switching to it. +/// +/// +/// When you have code like this: +/// +/// It's problematic because callers except that async methods can be called from any thread, per the 1st rule. +/// +public abstract class AbstractVSTHRD109AvoidAssertInAsyncMethodsAnalyzer : DiagnosticAnalyzer { - using System.Collections.Immutable; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; - using Microsoft.CodeAnalysis.Operations; + public const string Id = "VSTHRD109"; - /// - /// Report errors when async methods throw when not on the main thread instead of switching to it. - /// - /// - /// When you have code like this: - /// - /// It's problematic because callers except that async methods can be called from any thread, per the 1st rule. - /// - public abstract class AbstractVSTHRD109AvoidAssertInAsyncMethodsAnalyzer : DiagnosticAnalyzer - { - public const string Id = "VSTHRD109"; + internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD109_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD109_MessageFormat), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); - internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD109_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD109_MessageFormat), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); - /// - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); + protected abstract LanguageUtils LanguageUtils { get; } - private protected abstract LanguageUtils LanguageUtils { get; } + /// + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); - /// - public override void Initialize(AnalysisContext context) + context.RegisterCompilationStartAction(ctxt => { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + var mainThreadAssertingMethods = CommonInterest.ReadMethods(ctxt.Options, CommonInterest.FileNamePatternForMethodsThatAssertMainThread, ctxt.CancellationToken).ToImmutableArray(); + ctxt.RegisterOperationAction(Utils.DebuggableWrapper(c => this.AnalyzeInvocation(c, mainThreadAssertingMethods)), OperationKind.Invocation); + }); + } - context.RegisterCompilationStartAction(ctxt => - { - var mainThreadAssertingMethods = CommonInterest.ReadMethods(ctxt.Options, CommonInterest.FileNamePatternForMethodsThatAssertMainThread, ctxt.CancellationToken).ToImmutableArray(); - ctxt.RegisterOperationAction(Utils.DebuggableWrapper(c => this.AnalyzeInvocation(c, mainThreadAssertingMethods)), OperationKind.Invocation); - }); + private void AnalyzeInvocation(OperationAnalysisContext context, ImmutableArray mainThreadAssertingMethods) + { + if (!(Utils.GetContainingFunction(context.Operation, context.ContainingSymbol) is IMethodSymbol methodSymbol)) + { + return; } - private void AnalyzeInvocation(OperationAnalysisContext context, ImmutableArray mainThreadAssertingMethods) + if (methodSymbol.IsAsync || Utils.HasAsyncCompatibleReturnType(methodSymbol) || Utils.IsAsyncCompatibleReturnType(methodSymbol.ReturnType)) { - if (!(Utils.GetContainingFunction(context.Operation, context.ContainingSymbol) is IMethodSymbol methodSymbol)) - { - return; - } - - if (methodSymbol.IsAsync || Utils.HasAsyncCompatibleReturnType(methodSymbol) || Utils.IsAsyncCompatibleReturnType(methodSymbol.ReturnType)) + var invocation = (IInvocationOperation)context.Operation; + if (mainThreadAssertingMethods.Contains(invocation.TargetMethod)) { - var invocation = (IInvocationOperation)context.Operation; - if (mainThreadAssertingMethods.Contains(invocation.TargetMethod)) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptor, this.LanguageUtils.IsolateMethodName(invocation).GetLocation())); - } + context.ReportDiagnostic(Diagnostic.Create(Descriptor, this.LanguageUtils.IsolateMethodName(invocation).GetLocation())); } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD110ObserveResultOfAsyncCallsAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD110ObserveResultOfAsyncCallsAnalyzer.cs new file mode 100644 index 000000000..1bc7a2004 --- /dev/null +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD110ObserveResultOfAsyncCallsAnalyzer.cs @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +/// +/// Report errors when async methods calls are not awaited or the result used in some way within a synchronous method. +/// +public abstract class AbstractVSTHRD110ObserveResultOfAsyncCallsAnalyzer : DiagnosticAnalyzer +{ + public const string Id = "VSTHRD110"; + + internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD110_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD110_MessageFormat), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); + + protected abstract LanguageUtils LanguageUtils { get; } + + /// + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + + context.RegisterCompilationStartAction(context => + { + CommonInterest.AwaitableTypeTester awaitableTypes = CommonInterest.CollectAwaitableTypes(context.Compilation, context.CancellationToken); + context.RegisterOperationAction(Utils.DebuggableWrapper(context => this.AnalyzeInvocation(context, awaitableTypes)), OperationKind.Invocation); + }); + } + + /// + /// Determines whether an invocation is within a lambda expression that is being converted to an Expression tree. + /// + /// The invocation operation to check. + /// True if the invocation is within a lambda converted to an Expression; false otherwise. + private static bool IsWithinExpressionLambda(IInvocationOperation operation) + { + // Walk up the operation tree to find the containing lambda + IOperation? current = operation.Parent; + while (current is not null) + { + if (current is IAnonymousFunctionOperation lambda) + { + // Found a lambda, now check if it's being converted to an Expression<> + return IsLambdaConvertedToExpression(lambda); + } + + current = current.Parent; + } + + return false; + } + + /// + /// Determines whether a lambda is being converted to an Expression tree type. + /// + /// The lambda operation to check. + /// True if the lambda is being converted to an Expression; false otherwise. + private static bool IsLambdaConvertedToExpression(IAnonymousFunctionOperation lambda) + { + // Walk up from the lambda to find conversion or argument operations + IOperation? current = lambda.Parent; + while (current is not null) + { + // Check if the lambda's parent is a conversion operation + if (current is IConversionOperation conversion) + { + // Check if the target type is Expression<> or a related expression tree type + return IsExpressionTreeType(conversion.Type); + } + + // Check if the lambda is being passed as an argument to a method expecting Expression<> + if (current is IArgumentOperation argument && + argument.Parameter?.Type is INamedTypeSymbol parameterType) + { + return IsExpressionTreeType(parameterType); + } + + // Allow certain operations to be skipped (like parentheses) + if (current is IParenthesizedOperation) + { + current = current.Parent; + continue; + } + + // Stop walking up at other operation types to avoid false positives + break; + } + + return false; + } + + /// + /// Determines whether a type is an Expression tree type (Expression<T> or related types). + /// + /// The type to check. + /// True if the type is an Expression tree type; false otherwise. + private static bool IsExpressionTreeType(ITypeSymbol? type) + { + if (type is not INamedTypeSymbol namedType) + { + return false; + } + + // Check for System.Linq.Expressions.Expression + if (namedType.Name == "Expression" && + namedType.ContainingNamespace?.ToDisplayString() == "System.Linq.Expressions" && + namedType.IsGenericType) + { + return true; + } + + // Check for LambdaExpression and other expression types + if (namedType.ContainingNamespace?.ToDisplayString() == "System.Linq.Expressions" && + (namedType.Name == "LambdaExpression" || namedType.Name.EndsWith("Expression"))) + { + return true; + } + + return false; + } + + private void AnalyzeInvocation(OperationAnalysisContext context, CommonInterest.AwaitableTypeTester awaitableTypes) + { + var operation = (IInvocationOperation)context.Operation; + if (operation.Type is null) + { + return; + } + + if (operation.GetContainingFunction() is { } function && this.LanguageUtils.IsAsyncMethod(function.Syntax)) + { + // CS4014 should already take care of this case. + return; + } + + // Check if this invocation is within a lambda that's being converted to an Expression<> + if (IsWithinExpressionLambda(operation)) + { + // This invocation is within a lambda converted to an expression tree, so it's not actually being invoked. + return; + } + + // Only consider invocations that are direct statements (or are statements through limited steps). + // Otherwise, we assume their result is awaited, assigned, or otherwise consumed. + IOperation? parentOperation = operation.Parent; + while (parentOperation is not null) + { + if (parentOperation is IExpressionStatementOperation) + { + // This expression is directly used in a statement. + break; + } + + // This check is where we allow for specific operation types that may appear between the invocation + // and the statement that don't disqualify the invocation search for an invalid pattern. + if (parentOperation is IConditionalAccessOperation) + { + parentOperation = parentOperation.Parent; + } + else + { + // This expression is not directly used in a statement. + return; + } + } + + if (awaitableTypes.IsAwaitableType(operation.Type)) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptor, operation.Syntax.GetLocation())); + } + } +} diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD112ImplementSystemIAsyncDisposableAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD112ImplementSystemIAsyncDisposableAnalyzer.cs index 98507f275..56a9e1fc8 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD112ImplementSystemIAsyncDisposableAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD112ImplementSystemIAsyncDisposableAnalyzer.cs @@ -1,62 +1,61 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +/// +/// Verifies that types that implement vs-threading's IAsyncDisposable interface also implement System.IAsyncDisposable. +/// +public abstract class AbstractVSTHRD112ImplementSystemIAsyncDisposableAnalyzer : DiagnosticAnalyzer { - using System.Collections.Immutable; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; - - /// - /// Verifies that types that implement vs-threading's IAsyncDisposable interface also implement System.IAsyncDisposable. - /// - public abstract class AbstractVSTHRD112ImplementSystemIAsyncDisposableAnalyzer : DiagnosticAnalyzer - { - public const string Id = "VSTHRD112"; + public const string Id = "VSTHRD112"; - internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD112_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD112_MessageFormat), Strings.ResourceManager, typeof(Strings)), - description: new LocalizableResourceString(nameof(Strings.SystemIAsyncDisposablePackageNote), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Info, - isEnabledByDefault: true); + internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD112_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD112_MessageFormat), Strings.ResourceManager, typeof(Strings)), + description: new LocalizableResourceString(nameof(Strings.SystemIAsyncDisposablePackageNote), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true); - /// - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); - private protected abstract LanguageUtils LanguageUtils { get; } + protected abstract LanguageUtils LanguageUtils { get; } - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); - context.RegisterCompilationStartAction(startCompilation => + context.RegisterCompilationStartAction(startCompilation => + { + INamedTypeSymbol? vsThreadingAsyncDisposableType = startCompilation.Compilation.GetTypeByMetadataName(Types.IAsyncDisposable.FullName); + INamedTypeSymbol? bclAsyncDisposableType = startCompilation.Compilation.GetTypeByMetadataName(Types.BclAsyncDisposable.FullName); + if (vsThreadingAsyncDisposableType is object) { - INamedTypeSymbol? vsThreadingAsyncDisposableType = startCompilation.Compilation.GetTypeByMetadataName(Types.IAsyncDisposable.FullName); - INamedTypeSymbol? bclAsyncDisposableType = startCompilation.Compilation.GetTypeByMetadataName(Types.BclAsyncDisposable.FullName); - if (vsThreadingAsyncDisposableType is object) - { - startCompilation.RegisterSymbolAction(Utils.DebuggableWrapper(c => this.AnalyzeType(c, vsThreadingAsyncDisposableType, bclAsyncDisposableType)), SymbolKind.NamedType); - } - }); - } + startCompilation.RegisterSymbolAction(Utils.DebuggableWrapper(c => this.AnalyzeType(c, vsThreadingAsyncDisposableType, bclAsyncDisposableType)), SymbolKind.NamedType); + } + }); + } - private void AnalyzeType(SymbolAnalysisContext context, INamedTypeSymbol vsThreadingAsyncDisposableType, INamedTypeSymbol bclAsyncDisposableType) + private void AnalyzeType(SymbolAnalysisContext context, INamedTypeSymbol vsThreadingAsyncDisposableType, INamedTypeSymbol? bclAsyncDisposableType) + { + var symbol = (INamedTypeSymbol)context.Symbol; + if (symbol.Interfaces.Contains(vsThreadingAsyncDisposableType)) { - var symbol = (INamedTypeSymbol)context.Symbol; - if (symbol.Interfaces.Contains(vsThreadingAsyncDisposableType)) + if (bclAsyncDisposableType is null || !symbol.AllInterfaces.Contains(bclAsyncDisposableType)) { - if (bclAsyncDisposableType is null || !symbol.AllInterfaces.Contains(bclAsyncDisposableType)) - { - context.ReportDiagnostic( - Diagnostic.Create( - Descriptor, - this.LanguageUtils.GetLocationOfBaseTypeName(symbol, vsThreadingAsyncDisposableType, context.Compilation, context.CancellationToken))); - } + context.ReportDiagnostic( + Diagnostic.Create( + Descriptor, + this.LanguageUtils.GetLocationOfBaseTypeName(symbol, vsThreadingAsyncDisposableType, context.Compilation, context.CancellationToken))); } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD114AvoidReturningNullTaskAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD114AvoidReturningNullTaskAnalyzer.cs index c4c7df646..b677d1bfe 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD114AvoidReturningNullTaskAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/AbstractVSTHRD114AvoidReturningNullTaskAnalyzer.cs @@ -1,74 +1,96 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System.Collections.Immutable; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +/// +/// Finds await expressions on that do not use . +/// Also works on . +/// +public abstract class AbstractVSTHRD114AvoidReturningNullTaskAnalyzer : DiagnosticAnalyzer { - using System.Collections.Immutable; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; - using Microsoft.CodeAnalysis.Operations; - - /// - /// Finds await expressions on that do not use . - /// Also works on . - /// - public abstract class AbstractVSTHRD114AvoidReturningNullTaskAnalyzer : DiagnosticAnalyzer - { - public const string Id = "VSTHRD114"; + public const string Id = "VSTHRD114"; - internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD114_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD114_MessageFormat), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); + internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD114_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD114_MessageFormat), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); - /// - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); - private protected abstract LanguageUtils LanguageUtils { get; } + protected abstract LanguageUtils LanguageUtils { get; } - /// - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + /// + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); - context.RegisterOperationAction(Utils.DebuggableWrapper(context => this.AnalyzerReturnOperation(context)), OperationKind.Return); - } + context.RegisterOperationAction(Utils.DebuggableWrapper(context => this.AnalyzerReturnOperation(context)), OperationKind.Return); + } - private static IMethodSymbol? FindOwningSymbol(IBlockOperation block, ISymbol containingSymbol) + private static IMethodSymbol? FindOwningSymbol(IBlockOperation block, ISymbol containingSymbol) + { + return block.Parent switch { - return block.Parent switch - { - ILocalFunctionOperation localFunction => localFunction.Symbol, - IAnonymousFunctionOperation anonymousFunction => anonymousFunction.Symbol, + ILocalFunctionOperation localFunction => localFunction.Symbol, + IAnonymousFunctionOperation anonymousFunction => anonymousFunction.Symbol, - // Block parent is the method declaration, for vbnet this means a null parent but for C# it's a IMethodBodyOperation - null => containingSymbol as IMethodSymbol, - IMethodBodyOperation _ => containingSymbol as IMethodSymbol, + // Block parent is the method declaration, for vbnet this means a null parent but for C# it's a IMethodBodyOperation + null => containingSymbol as IMethodSymbol, + IMethodBodyOperation _ => containingSymbol as IMethodSymbol, - _ => null, - }; - } + _ => null, + }; + } - private void AnalyzerReturnOperation(OperationAnalysisContext context) + private static void CheckForNullValue(OperationAnalysisContext context, IOperation operation) + { + if (operation is IConditionalOperation conditionalOp) { - var returnOperation = (IReturnOperation)context.Operation; - - if (returnOperation.ReturnedValue is { ConstantValue: { HasValue: true, Value: null } } && // could be null for implicit returns - returnOperation.ReturnedValue.Syntax is { } returnedValueSyntax && - Utils.GetContainingFunctionBlock(returnOperation) is { } block && - FindOwningSymbol(block, context.ContainingSymbol) is { } method && - !method.IsAsync && - Utils.IsTask(method.ReturnType) && - !this.LanguageUtils.MethodReturnsNullableReferenceType(method)) + if (conditionalOp.WhenTrue is { } whenTrue) { - context.ReportDiagnostic(Diagnostic.Create(Descriptor, returnedValueSyntax.GetLocation())); + CheckForNullValue(context, whenTrue); } + + if (conditionalOp.WhenFalse is { } whenFalse) + { + CheckForNullValue(context, whenFalse); + } + } + else if (operation.ConstantValue is { HasValue: true, Value: null } && + operation.Syntax is { } nullSyntax) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptor, nullSyntax.GetLocation())); + } + } + + private void AnalyzerReturnOperation(OperationAnalysisContext context) + { + var returnOperation = (IReturnOperation)context.Operation; + + // ReturnedValue is null for implicit/void returns + if (returnOperation.ReturnedValue is not { } returnedValue || + Utils.GetContainingFunctionBlock(returnOperation) is not { } block || + FindOwningSymbol(block, context.ContainingSymbol) is not { } owningMethod || + owningMethod.IsAsync || + !Utils.IsTask(owningMethod.ReturnType) || + this.LanguageUtils.MethodReturnsNullableReferenceType(owningMethod)) + { + return; } + + CheckForNullValue(context, returnedValue); } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/AssemblyInfo.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/AssemblyInfo.cs index bf2463b3f..61f8ea2f6 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/AssemblyInfo.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/AssemblyInfo.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -10,8 +10,3 @@ [assembly: ComVisible(false)] [assembly: NeutralResourcesLanguage("en-US")] - -[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Threading.Analyzers.CodeFixes, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] -[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Threading.Analyzers.CSharp, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] -[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Threading.Analyzers.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] -[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Threading.Analyzers.VisualBasic, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/CommonInterest.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/CommonInterest.cs index acab90219..b4fdfc07a 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/CommonInterest.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/CommonInterest.cs @@ -1,343 +1,551 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +public static class CommonInterest { - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Diagnostics; - using System.Diagnostics.CodeAnalysis; - using System.IO; - using System.Linq; - using System.Runtime.CompilerServices; - using System.Text.RegularExpressions; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; - using Microsoft.CodeAnalysis.Text; - - internal static class CommonInterest + public static readonly Regex FileNamePatternForLegacyThreadSwitchingMembers = new Regex(@"^vs-threading\.LegacyThreadSwitchingMembers(\..*)?.txt$", FileNamePatternRegexOptions); + public static readonly Regex FileNamePatternForMembersRequiringMainThread = new Regex(@"^vs-threading\.MembersRequiringMainThread(\..*)?.txt$", FileNamePatternRegexOptions); + public static readonly Regex FileNamePatternForMethodsThatAssertMainThread = new Regex(@"^vs-threading\.MainThreadAssertingMethods(\..*)?.txt$", FileNamePatternRegexOptions); + public static readonly Regex FileNamePatternForMethodsThatSwitchToMainThread = new Regex(@"^vs-threading\.MainThreadSwitchingMethods(\..*)?.txt$", FileNamePatternRegexOptions); + public static readonly Regex FileNamePatternForSyncMethodsToExcludeFromVSTHRD103 = new Regex(@"^vs-threading\.SyncMethodsToExcludeFromVSTHRD103(\..*)?.txt$", FileNamePatternRegexOptions); + + public static readonly IEnumerable JTFSyncBlockers = + [ + new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.MicrosoftVisualStudioThreading, Types.JoinableTaskFactory.TypeName), Types.JoinableTaskFactory.Run), Types.JoinableTaskFactory.RunAsync), + new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.MicrosoftVisualStudioThreading, Types.JoinableTask.TypeName), Types.JoinableTask.Join), Types.JoinableTask.JoinAsync), + ]; + + public static readonly IEnumerable ProblematicSyncBlockingMethods = + [ + new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemThreadingTasks, nameof(Task)), nameof(Task.Wait)), null), + new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemThreadingTasks, nameof(Task)), nameof(Task.WaitAll)), null), + new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemThreadingTasks, nameof(Task)), nameof(Task.WaitAny)), null), + new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemRuntimeCompilerServices, nameof(ConfiguredTaskAwaitable.ConfiguredTaskAwaiter)), nameof(ConfiguredTaskAwaitable.ConfiguredTaskAwaiter.GetResult)), null), + new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemRuntimeCompilerServices, nameof(TaskAwaiter)), nameof(TaskAwaiter.GetResult)), null), + new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemRuntimeCompilerServices, nameof(ValueTaskAwaiter)), nameof(ValueTaskAwaiter.GetResult)), null), + new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemRuntimeCompilerServices, nameof(ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter)), nameof(ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter.GetResult)), null), + ]; + + public static readonly IEnumerable SyncBlockingMethods = JTFSyncBlockers.Concat(ProblematicSyncBlockingMethods).Concat( + [ + new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.MicrosoftVisualStudioShellInterop, "IVsTask"), "Wait"), extensionMethodNamespace: Namespaces.MicrosoftVisualStudioShell), + new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.MicrosoftVisualStudioShellInterop, "IVsTask"), "GetResult"), extensionMethodNamespace: Namespaces.MicrosoftVisualStudioShell), + ]); + + public static readonly ImmutableArray SyncBlockingProperties = + [ + new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemThreadingTasks, nameof(Task)), nameof(Task.Result)), null), + new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemThreadingTasks, nameof(ValueTask)), nameof(ValueTask.Result)), null), + ]; + + public static readonly IEnumerable ThreadAffinityTestingMethods = + [ + new QualifiedMember(new QualifiedType(Namespaces.MicrosoftVisualStudioShell, Types.ThreadHelper.TypeName), Types.ThreadHelper.CheckAccess), + ]; + + public static readonly ImmutableArray TaskConfigureAwait = ImmutableArray.Create( + new QualifiedMember(new QualifiedType(Types.Task.Namespace, Types.Task.TypeName), nameof(Task.ConfigureAwait)), + new QualifiedMember(new QualifiedType(Types.AwaitExtensions.Namespace, Types.AwaitExtensions.TypeName), Types.AwaitExtensions.ConfigureAwaitRunInline)); + + private const RegexOptions FileNamePatternRegexOptions = RegexOptions.IgnoreCase | RegexOptions.Singleline; + + private const string GetAwaiterMethodName = "GetAwaiter"; + + public static IEnumerable ReadMethods(AnalyzerOptions analyzerOptions, Regex fileNamePattern, CancellationToken cancellationToken) { - internal static readonly Regex FileNamePatternForLegacyThreadSwitchingMembers = new Regex(@"^vs-threading\.LegacyThreadSwitchingMembers(\..*)?.txt$", FileNamePatternRegexOptions); - internal static readonly Regex FileNamePatternForMembersRequiringMainThread = new Regex(@"^vs-threading\.MembersRequiringMainThread(\..*)?.txt$", FileNamePatternRegexOptions); - internal static readonly Regex FileNamePatternForMethodsThatAssertMainThread = new Regex(@"^vs-threading\.MainThreadAssertingMethods(\..*)?.txt$", FileNamePatternRegexOptions); - internal static readonly Regex FileNamePatternForMethodsThatSwitchToMainThread = new Regex(@"^vs-threading\.MainThreadSwitchingMethods(\..*)?.txt$", FileNamePatternRegexOptions); - - internal static readonly IEnumerable JTFSyncBlockers = new[] + foreach (string line in ReadAdditionalFiles(analyzerOptions, fileNamePattern, cancellationToken)) { - new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.MicrosoftVisualStudioThreading, Types.JoinableTaskFactory.TypeName), Types.JoinableTaskFactory.Run), Types.JoinableTaskFactory.RunAsync), - new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.MicrosoftVisualStudioThreading, Types.JoinableTask.TypeName), Types.JoinableTask.Join), Types.JoinableTask.JoinAsync), - }; + yield return ParseAdditionalFileMethodLine(line); + } + } - internal static readonly IEnumerable ProblematicSyncBlockingMethods = new[] + public static IEnumerable ReadTypesAndMembers(AnalyzerOptions analyzerOptions, Regex fileNamePattern, CancellationToken cancellationToken) + { + foreach (string line in ReadAdditionalFiles(analyzerOptions, fileNamePattern, cancellationToken)) { - new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemThreadingTasks, nameof(Task)), nameof(Task.Wait)), null), - new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemThreadingTasks, nameof(Task)), nameof(Task.WaitAll)), null), - new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemThreadingTasks, nameof(Task)), nameof(Task.WaitAny)), null), - new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemRuntimeCompilerServices, nameof(TaskAwaiter)), nameof(TaskAwaiter.GetResult)), null), - new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemRuntimeCompilerServices, nameof(ValueTaskAwaiter)), nameof(ValueTaskAwaiter.GetResult)), null), - }; + if (!CommonInterestParsing.TryParseNegatableTypeOrMemberReference(line, out bool negated, out ReadOnlyMemory typeNameMemory, out string? memberNameValue)) + { + throw new InvalidOperationException($"Parsing error on line: {line}"); + } - internal static readonly IEnumerable SyncBlockingMethods = JTFSyncBlockers.Concat(ProblematicSyncBlockingMethods).Concat(new[] - { - new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.MicrosoftVisualStudioShellInterop, "IVsTask"), "Wait"), extensionMethodNamespace: Namespaces.MicrosoftVisualStudioShell), - new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.MicrosoftVisualStudioShellInterop, "IVsTask"), "GetResult"), extensionMethodNamespace: Namespaces.MicrosoftVisualStudioShell), - }); + (ImmutableArray containingNamespace, string? typeName) = SplitQualifiedIdentifier(typeNameMemory); + var type = new QualifiedType(containingNamespace, typeName); + QualifiedMember member = memberNameValue is not null ? new QualifiedMember(type, memberNameValue) : default(QualifiedMember); + yield return new TypeMatchSpec(type, member, negated); + } + } - internal static readonly IReadOnlyList SyncBlockingProperties = new[] + public static IEnumerable ReadAdditionalFiles(AnalyzerOptions analyzerOptions, Regex fileNamePattern, CancellationToken cancellationToken) + { + if (analyzerOptions is null) { - new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemThreadingTasks, nameof(Task)), nameof(Task.Result)), null), - new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemThreadingTasks, nameof(ValueTask)), nameof(ValueTask.Result)), null), - }; + throw new ArgumentNullException(nameof(analyzerOptions)); + } - internal static readonly IEnumerable ThreadAffinityTestingMethods = new[] + if (fileNamePattern is null) { - new QualifiedMember(new QualifiedType(Namespaces.MicrosoftVisualStudioShell, Types.ThreadHelper.TypeName), Types.ThreadHelper.CheckAccess), - }; + throw new ArgumentNullException(nameof(fileNamePattern)); + } - internal static readonly ImmutableArray TaskConfigureAwait = ImmutableArray.Create( - new QualifiedMember(new QualifiedType(Types.Task.Namespace, Types.Task.TypeName), nameof(Task.ConfigureAwait)), - new QualifiedMember(new QualifiedType(Types.AwaitExtensions.Namespace, Types.AwaitExtensions.TypeName), Types.AwaitExtensions.ConfigureAwaitRunInline)); + IEnumerable? docs = from file in analyzerOptions.AdditionalFiles.OrderBy(x => x.Path, StringComparer.Ordinal) + let fileName = Path.GetFileName(file.Path) + where fileNamePattern.IsMatch(fileName) + let text = file.GetText(cancellationToken) + select text; + return docs.SelectMany(ReadLinesFromAdditionalFile); + } - private const RegexOptions FileNamePatternRegexOptions = RegexOptions.IgnoreCase | RegexOptions.Singleline; + public static bool Contains(this ImmutableArray methods, ISymbol symbol) + { + foreach (QualifiedMember method in methods) + { + if (method.IsMatch(symbol)) + { + return true; + } + } - private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(5); // Prevent expensive CPU hang in Regex.Match if backtracking occurs due to pathological input (see #485). + return false; + } - private static readonly Regex NegatableTypeOrMemberReferenceRegex = new Regex(@"^(?!)?\[(?[^\[\]\:]+)+\](?:\:\:(?\S+))?\s*$", RegexOptions.Singleline | RegexOptions.CultureInvariant, RegexMatchTimeout); + public static bool Contains(this ImmutableArray types, [NotNullWhen(true)] ITypeSymbol? typeSymbol, ISymbol? memberSymbol) + { + TypeMatchSpec matching = default(TypeMatchSpec); + foreach (TypeMatchSpec type in types) + { + if (type.IsMatch(typeSymbol, memberSymbol)) + { + if (matching.IsEmpty || matching.IsWildcard) + { + matching = type; + if (!matching.IsWildcard) + { + // It's an exact match, so return it immediately. + return !matching.InvertedLogic; + } + } + } + } - private static readonly Regex MemberReferenceRegex = new Regex(@"^\[(?[^\[\]\:]+)+\]::(?\S+)\s*$", RegexOptions.Singleline | RegexOptions.CultureInvariant, RegexMatchTimeout); + return !matching.IsEmpty && !matching.InvertedLogic; + } - /// - /// An array with '.' as its only element. - /// - private static readonly char[] QualifiedIdentifierSeparators = new[] { '.' }; + public static CommonInterest.AwaitableTypeTester CollectAwaitableTypes(Compilation compilation, CancellationToken cancellationToken) + { + HashSet awaitableTypes = new(SymbolEqualityComparer.Default); + void AddAwaitableType(ITypeSymbol type) + { + if (type is INamedTypeSymbol { IsGenericType: true, IsUnboundGenericType: false } genericType) + { + awaitableTypes.Add(genericType.ConstructUnboundGenericType()); + } + else + { + awaitableTypes.Add(type); + } + } - internal static IEnumerable ReadMethods(AnalyzerOptions analyzerOptions, Regex fileNamePattern, CancellationToken cancellationToken) + foreach (ISymbol getAwaiterMember in compilation.GetSymbolsWithName(GetAwaiterMethodName, SymbolFilter.Member, cancellationToken)) { - foreach (string line in ReadAdditionalFiles(analyzerOptions, fileNamePattern, cancellationToken)) + if (TryGetAwaitableType(getAwaiterMember, out ITypeSymbol? awaitableType)) { - yield return ParseAdditionalFileMethodLine(line); + AddAwaitableType(awaitableType); } } - internal static IEnumerable ReadTypesAndMembers(AnalyzerOptions analyzerOptions, Regex fileNamePattern, CancellationToken cancellationToken) + foreach (IAssemblySymbol referenceAssembly in compilation.Assembly.Modules.First().ReferencedAssemblySymbols) { - foreach (string line in ReadAdditionalFiles(analyzerOptions, fileNamePattern, cancellationToken)) + CollectNamespace(referenceAssembly.GlobalNamespace); + + void CollectNamespace(INamespaceOrTypeSymbol nsOrType) { - Match? match = null; - try + if (nsOrType is INamespaceSymbol ns) { - match = NegatableTypeOrMemberReferenceRegex.Match(line); + foreach (INamespaceSymbol nestedNs in ns.GetNamespaceMembers()) + { + CollectNamespace(nestedNs); + } } - catch (RegexMatchTimeoutException) + + foreach (INamedTypeSymbol nestedType in nsOrType.GetTypeMembers()) { - throw new InvalidOperationException($"Regex.Match timeout when parsing line: {line}"); + CollectNamespace(nestedType); } - if (!match.Success) + foreach (ISymbol getAwaiterMember in nsOrType.GetMembers(GetAwaiterMethodName)) { - throw new InvalidOperationException($"Parsing error on line: {line}"); + if (TryGetAwaitableType(getAwaiterMember, out ITypeSymbol? awaitableType)) + { + AddAwaitableType(awaitableType); + } } - - bool inverted = match.Groups["negated"].Success; - string[] typeNameElements = match.Groups["typeName"].Value.Split(QualifiedIdentifierSeparators); - string typeName = typeNameElements[typeNameElements.Length - 1]; - var containingNamespace = typeNameElements.Take(typeNameElements.Length - 1).ToImmutableArray(); - var type = new QualifiedType(containingNamespace, typeName); - QualifiedMember member = match.Groups["memberName"].Success ? new QualifiedMember(type, match.Groups["memberName"].Value) : default(QualifiedMember); - yield return new TypeMatchSpec(type, member, inverted); } } - internal static IEnumerable ReadAdditionalFiles(AnalyzerOptions analyzerOptions, Regex fileNamePattern, CancellationToken cancellationToken) + bool TryGetAwaitableType(ISymbol getAwaiterMember, [NotNullWhen(true)] out ITypeSymbol? awaitableType) { - if (analyzerOptions is null) + if (getAwaiterMember is IMethodSymbol getAwaiterMethod && compilation.IsSymbolAccessibleWithin(getAwaiterMember, compilation.Assembly) && TestGetAwaiterMethod(getAwaiterMethod)) { - throw new ArgumentNullException(nameof(analyzerOptions)); + awaitableType = getAwaiterMethod.IsExtensionMethod ? getAwaiterMethod.Parameters[0].Type : getAwaiterMethod.ContainingType; + return true; } - if (fileNamePattern is null) - { - throw new ArgumentNullException(nameof(fileNamePattern)); - } + awaitableType = null; + return false; + } - IEnumerable? docs = from file in analyzerOptions.AdditionalFiles.OrderBy(x => x.Path, StringComparer.Ordinal) - let fileName = Path.GetFileName(file.Path) - where fileNamePattern.IsMatch(fileName) - let text = file.GetText(cancellationToken) - select text; - return docs.SelectMany(ReadLinesFromAdditionalFile); + return new AwaitableTypeTester(awaitableTypes); + } + + public static bool IsAwaitable(this ITypeSymbol? typeSymbol) + { + if (typeSymbol is null) + { + return false; } - internal static bool Contains(this ImmutableArray methods, ISymbol symbol) + foreach (ISymbol symbol in typeSymbol.GetMembers(GetAwaiterMethodName)) { - foreach (QualifiedMember method in methods) + if (symbol is IMethodSymbol getAwaiterMethod && TestGetAwaiterMethod(getAwaiterMethod)) { - if (method.IsMatch(symbol)) - { - return true; - } + return true; } + } + + return false; + } + public static bool IsAwaitable(this ITypeSymbol? typeSymbol, SemanticModel semanticModel, int position) + { + if (typeSymbol is null) + { return false; } - internal static bool Contains(this ImmutableArray types, [NotNullWhen(true)] ITypeSymbol? typeSymbol, ISymbol? memberSymbol) + // We're able to do a more comprehensive job by detecting extension methods as well when we have a semantic model. + foreach (ISymbol symbol in semanticModel.LookupSymbols(position, typeSymbol, name: GetAwaiterMethodName, includeReducedExtensionMethods: true)) { - TypeMatchSpec matching = default(TypeMatchSpec); - foreach (TypeMatchSpec type in types) + if (symbol is IMethodSymbol getAwaiterMethod && TestGetAwaiterMethod(getAwaiterMethod)) { - if (type.IsMatch(typeSymbol, memberSymbol)) - { - if (matching.IsEmpty || matching.IsWildcard) - { - matching = type; - if (!matching.IsWildcard) - { - // It's an exact match, so return it immediately. - return !matching.InvertedLogic; - } - } - } + return true; } - - return !matching.IsEmpty && !matching.InvertedLogic; } - internal static IEnumerable ReadLinesFromAdditionalFile(SourceText text) + return false; + } + + public static IOperation? GetContainingFunction(this IOperation? operation) + { + while (operation?.Parent is not null) { - if (text is null) + if (operation.Parent is IAnonymousFunctionOperation or IMethodBodyOperation or ILocalFunctionOperation) { - throw new ArgumentNullException(nameof(text)); + return operation.Parent; } - foreach (TextLine line in text.Lines) + if (operation.Language == LanguageNames.VisualBasic) { - string lineText = line.ToString(); - - if (!string.IsNullOrWhiteSpace(lineText) && !lineText.StartsWith("#", StringComparison.OrdinalIgnoreCase)) + if (operation.Parent is IBlockOperation) { - yield return lineText; + return operation.Parent; } } + + operation = operation.Parent; + } + + return null; + } + + public static bool ConformsToAwaiterPattern(ITypeSymbol typeSymbol) + { + if (typeSymbol is null) + { + return false; } - internal static QualifiedMember ParseAdditionalFileMethodLine(string line) + var hasGetResultMethod = false; + var hasOnCompletedMethod = false; + var hasIsCompletedProperty = false; + + foreach (ISymbol? member in typeSymbol.GetMembers()) { - Match? match = null; - try - { - match = MemberReferenceRegex.Match(line); - } - catch (RegexMatchTimeoutException) - { - throw new InvalidOperationException($"Regex.Match timeout when parsing line: {line}"); - } + hasGetResultMethod |= member.Name == nameof(TaskAwaiter.GetResult) && member is IMethodSymbol m && m.Parameters.IsEmpty; + hasOnCompletedMethod |= member.Name == nameof(TaskAwaiter.OnCompleted) && member is IMethodSymbol; + hasIsCompletedProperty |= member.Name == nameof(TaskAwaiter.IsCompleted) && member is IPropertySymbol; - if (!match.Success) + if (hasGetResultMethod && hasOnCompletedMethod && hasIsCompletedProperty) { - throw new InvalidOperationException($"Parsing error on line: {line}"); + return true; } + } + + return false; + } - string methodName = match.Groups["memberName"].Value; - string[] typeNameElements = match.Groups["typeName"].Value.Split(QualifiedIdentifierSeparators); - string typeName = typeNameElements[typeNameElements.Length - 1]; - var containingType = new QualifiedType(typeNameElements.Take(typeNameElements.Length - 1).ToImmutableArray(), typeName); - return new QualifiedMember(containingType, methodName); + public static IEnumerable ReadLinesFromAdditionalFile(SourceText text) + { + if (text is null) + { + throw new ArgumentNullException(nameof(text)); } - internal readonly struct TypeMatchSpec + foreach (TextLine line in text.Lines) { - internal TypeMatchSpec(QualifiedType type, QualifiedMember member, bool inverted) - { - this.InvertedLogic = inverted; - this.Type = type; - this.Member = member; + string lineText = line.ToString(); - if (this.IsWildcard && this.Member.Name is object) - { - throw new ArgumentException("Wildcard use is not allowed when member of type is specified."); - } + if (!string.IsNullOrWhiteSpace(lineText) && !lineText.StartsWith("#", StringComparison.OrdinalIgnoreCase)) + { + yield return lineText; } + } + } + + public static QualifiedMember ParseAdditionalFileMethodLine(string line) + { + if (!CommonInterestParsing.TryParseMemberReference(line, out ReadOnlyMemory typeNameMemory, out string? memberName)) + { + throw new InvalidOperationException($"Parsing error on line: {line}"); + } - /// - /// Gets a value indicating whether this entry appeared in a file with a leading "!" character. - /// - internal bool InvertedLogic { get; } - - /// - /// Gets the type described by this entry. - /// - internal QualifiedType Type { get; } - - /// - /// Gets the member described by this entry. - /// - internal QualifiedMember Member { get; } - - /// - /// Gets a value indicating whether a member match is reuqired. - /// - internal bool IsMember => this.Member.Name is object; - - /// - /// Gets a value indicating whether the typename is a wildcard. - /// - internal bool IsWildcard => this.Type.Name == "*"; - - /// - /// Gets a value indicating whether this is an uninitialized (default) instance. - /// - internal bool IsEmpty => this.Type.Namespace is null; - - /// - /// Tests whether a given symbol matches the description of a type (independent of its property). - /// - internal bool IsMatch([NotNullWhen(true)] ITypeSymbol? typeSymbol, ISymbol? memberSymbol) + (ImmutableArray containingNamespace, string? typeName) = SplitQualifiedIdentifier(typeNameMemory); + var containingType = new QualifiedType(containingNamespace, typeName); + return new QualifiedMember(containingType, memberName!); + } + + /// + /// Splits a qualified type name (e.g. My.Namespace.MyType) into its containing namespace + /// segments and the simple type name, without allocating an intermediate joined string. + /// + /// The qualified type name as a memory slice. + /// The namespace segments and the simple type name. + private static (ImmutableArray ContainingNamespace, string TypeName) SplitQualifiedIdentifier(ReadOnlyMemory qualifiedName) + { + int lastDot = qualifiedName.Span.LastIndexOf('.'); + if (lastDot < 0) + { + return (ImmutableArray.Empty, qualifiedName.ToString()); + } + + string typeName = qualifiedName.Slice(lastDot + 1).ToString(); + ReadOnlyMemory nsPart = qualifiedName.Slice(0, lastDot); + ImmutableArray.Builder nsBuilder = ImmutableArray.CreateBuilder(); + while (!nsPart.IsEmpty) + { + int dot = nsPart.Span.IndexOf('.'); + if (dot < 0) { - if (typeSymbol is null) - { - return false; - } + nsBuilder.Add(nsPart.ToString()); + break; + } - if (!this.IsMember - && (this.IsWildcard || typeSymbol.Name == this.Type.Name) - && typeSymbol.BelongsToNamespace(this.Type.Namespace)) - { - return true; - } + nsBuilder.Add(nsPart.Slice(0, dot).ToString()); + nsPart = nsPart.Slice(dot + 1); + } - if (this.IsMember - && memberSymbol?.Name == this.Member.Name - && typeSymbol.Name == this.Type.Name - && typeSymbol.BelongsToNamespace(this.Type.Namespace)) - { - return true; - } + return (nsBuilder.ToImmutable(), typeName); + } + private static bool TestGetAwaiterMethod(IMethodSymbol getAwaiterMethod) + { + if (getAwaiterMethod.IsExtensionMethod) + { + if (getAwaiterMethod.Parameters.Length != 1) + { return false; } } - - internal readonly struct QualifiedType + else { - public QualifiedType(IReadOnlyList containingTypeNamespace, string typeName) + if (!getAwaiterMethod.Parameters.IsEmpty) { - this.Namespace = containingTypeNamespace; - this.Name = typeName; + return false; } + } + + if (!CommonInterest.ConformsToAwaiterPattern(getAwaiterMethod.ReturnType)) + { + return false; + } - public IReadOnlyList Namespace { get; } + return true; + } - public string Name { get; } + public readonly struct TypeMatchSpec + { + public TypeMatchSpec(QualifiedType type, QualifiedMember member, bool inverted) + { + this.InvertedLogic = inverted; + this.Type = type; + this.Member = member; - public bool IsMatch(ISymbol symbol) + if (this.IsWildcard && this.Member.Name is object) { - return symbol?.Name == this.Name - && symbol.BelongsToNamespace(this.Namespace); + throw new ArgumentException("Wildcard use is not allowed when member of type is specified."); } - - public override string ToString() => string.Join(".", this.Namespace.Concat(new[] { this.Name })); } - internal readonly struct QualifiedMember + /// + /// Gets a value indicating whether this entry appeared in a file with a leading "!" character. + /// + public bool InvertedLogic { get; } + + /// + /// Gets the type described by this entry. + /// + public QualifiedType Type { get; } + + /// + /// Gets the member described by this entry. + /// + public QualifiedMember Member { get; } + + /// + /// Gets a value indicating whether a member match is required. + /// + public bool IsMember => this.Member.Name is object; + + /// + /// Gets a value indicating whether the typename is a wildcard. + /// + public bool IsWildcard => this.Type.Name == "*"; + + /// + /// Gets a value indicating whether this is an uninitialized (default) instance. + /// + public bool IsEmpty => this.Type.Name is null; + + /// + /// Tests whether a given symbol matches the description of a type (independent of its property). + /// + public bool IsMatch([NotNullWhen(true)] ITypeSymbol? typeSymbol, ISymbol? memberSymbol) { - public QualifiedMember(QualifiedType containingType, string methodName) + if (typeSymbol is null) { - this.ContainingType = containingType; - this.Name = methodName; + return false; } - public QualifiedType ContainingType { get; } - - public string Name { get; } + if (!this.IsMember + && (this.IsWildcard || typeSymbol.Name == this.Type.Name) + && typeSymbol.BelongsToNamespace(this.Type.Namespace)) + { + return true; + } - public bool IsMatch(ISymbol symbol) + if (this.IsMember + && memberSymbol?.Name == this.Member.Name + && typeSymbol.Name == this.Type.Name + && typeSymbol.BelongsToNamespace(this.Type.Namespace)) { - return symbol?.Name == this.Name - && this.ContainingType.IsMatch(symbol.ContainingType); + return true; } - public override string ToString() => this.ContainingType.ToString() + "." + this.Name; + return false; + } + } + + public readonly struct QualifiedType + { + public QualifiedType(ImmutableArray containingTypeNamespace, string typeName) + { + this.Namespace = containingTypeNamespace; + this.Name = typeName; + } + + public ImmutableArray Namespace { get; } + + public string Name { get; } + + public bool IsMatch(ISymbol symbol) + { + return symbol?.Name == this.Name + && symbol.BelongsToNamespace(this.Namespace); + } + + public override string ToString() => string.Join(".", this.Namespace.Concat([this.Name])); + } + + public readonly struct QualifiedMember + { + public QualifiedMember(QualifiedType containingType, string methodName) + { + this.ContainingType = containingType; + this.Name = methodName; + } + + public QualifiedType ContainingType { get; } + + public string Name { get; } + + public bool IsMatch(ISymbol? symbol) + { + return symbol?.Name == this.Name + && this.ContainingType.IsMatch(symbol.ContainingType); } - [DebuggerDisplay("{" + nameof(Method) + "} -> {" + nameof(AsyncAlternativeMethodName) + "}")] - internal readonly struct SyncBlockingMethod + public override string ToString() => this.ContainingType.ToString() + "." + this.Name; + } + + [DebuggerDisplay("{" + nameof(Method) + "} -> {" + nameof(AsyncAlternativeMethodName) + "}")] + public readonly struct SyncBlockingMethod + { + public SyncBlockingMethod(QualifiedMember method, string? asyncAlternativeMethodName = null, ImmutableArray? extensionMethodNamespace = null) { - public SyncBlockingMethod(QualifiedMember method, string? asyncAlternativeMethodName = null, IReadOnlyList? extensionMethodNamespace = null) + this.Method = method; + this.AsyncAlternativeMethodName = asyncAlternativeMethodName; + this.ExtensionMethodNamespace = extensionMethodNamespace; + } + + public QualifiedMember Method { get; } + + public string? AsyncAlternativeMethodName { get; } + + public ImmutableArray? ExtensionMethodNamespace { get; } + } + + public class AwaitableTypeTester + { + private readonly HashSet awaitableTypes; + + public AwaitableTypeTester(HashSet awaitableTypes) + { + this.awaitableTypes = awaitableTypes; + } + + public bool IsAwaitableType(ITypeSymbol typeSymbol) + { + if (this.awaitableTypes.Contains(typeSymbol)) { - this.Method = method; - this.AsyncAlternativeMethodName = asyncAlternativeMethodName; - this.ExtensionMethodNamespace = extensionMethodNamespace; + return true; } - public QualifiedMember Method { get; } - - public string? AsyncAlternativeMethodName { get; } + if (typeSymbol is INamedTypeSymbol { IsGenericType: true } genericTypeSymbol) + { + if (this.awaitableTypes.Contains(genericTypeSymbol.ConstructUnboundGenericType())) + { + return true; + } + } - public IReadOnlyList? ExtensionMethodNamespace { get; } + return false; } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/CommonInterestParsing.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/CommonInterestParsing.cs new file mode 100644 index 000000000..7088ab858 --- /dev/null +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/CommonInterestParsing.cs @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +/// +/// Parsing helpers for additional-file lines used by . +/// This class is intentionally kept free of Roslyn dependencies so it can be +/// linked directly into test projects for unit testing. +/// +internal static class CommonInterestParsing +{ + /// + /// Parses a line that may begin with an optional !, followed by [TypeName], + /// and optionally ::MemberName, without using regular expressions. + /// + /// The line to parse. + /// if the line begins with '!'. + /// The type name parsed from the brackets. + /// The member name after '::', or if not present. + /// if parsing succeeded. + internal static bool TryParseNegatableTypeOrMemberReference(string line, out bool negated, out ReadOnlyMemory typeName, out string? memberName) + { + negated = false; + typeName = default; + memberName = null; + + ReadOnlySpan span = line.AsSpan(); + int pos = 0; + + // Optional negation prefix. + if (pos < span.Length && span[pos] == '!') + { + negated = true; + pos++; + } + + // Required '[TypeName]'. + int bracketStart = pos; + if (!TryParseBracketedTypeName(span, ref pos, out _)) + { + return false; + } + + // Compute memory slice for type name (between the brackets), using recorded bracket position. + ReadOnlyMemory typeNameMemory = line.AsMemory(bracketStart + 1, pos - bracketStart - 2); + + // Optional '::memberName'. + ReadOnlySpan memberNameSpan = default; + if (pos + 1 < span.Length && span[pos] == ':' && span[pos + 1] == ':') + { + pos += 2; + int memberNameStart = pos; + while (pos < span.Length && !char.IsWhiteSpace(span[pos])) + { + pos++; + } + + if (pos == memberNameStart) + { + // '::' present but no member name follows. + return false; + } + + memberNameSpan = span.Slice(memberNameStart, pos - memberNameStart); + } + + // Allow only trailing whitespace. + while (pos < span.Length && char.IsWhiteSpace(span[pos])) + { + pos++; + } + + if (pos != span.Length) + { + return false; + } + + // Only allocate strings after full validation. + typeName = typeNameMemory; + memberName = memberNameSpan.IsEmpty ? null : memberNameSpan.ToString(); + return true; + } + + /// + /// Parses a line of the form [TypeName]::MemberName, without using regular expressions. + /// + /// The line to parse. + /// The type name parsed from the brackets. + /// The member name after '::'. + /// if parsing succeeded. + internal static bool TryParseMemberReference(string line, out ReadOnlyMemory typeName, out string? memberName) + { + typeName = default; + memberName = null; + + ReadOnlySpan span = line.AsSpan(); + int pos = 0; + + // Required '[TypeName]'. + int bracketStart = pos; + if (!TryParseBracketedTypeName(span, ref pos, out _)) + { + return false; + } + + // Compute memory slice for type name (between the brackets), using recorded bracket position. + ReadOnlyMemory typeNameMemory = line.AsMemory(bracketStart + 1, pos - bracketStart - 2); + + // Required '::'. + if (pos + 1 >= span.Length || span[pos] != ':' || span[pos + 1] != ':') + { + return false; + } + + pos += 2; + + // Member name: one or more non-whitespace chars. + int memberNameStart = pos; + while (pos < span.Length && !char.IsWhiteSpace(span[pos])) + { + pos++; + } + + if (pos == memberNameStart) + { + return false; + } + + ReadOnlySpan memberNameSpan = span.Slice(memberNameStart, pos - memberNameStart); + + // Allow only trailing whitespace. + while (pos < span.Length && char.IsWhiteSpace(span[pos])) + { + pos++; + } + + if (pos != span.Length) + { + return false; + } + + // Only allocate strings after full validation. + typeName = typeNameMemory; + memberName = memberNameSpan.ToString(); + return true; + } + + /// + /// Advances past a [TypeName] token and outputs the type-name span. + /// + /// The full input span. + /// The current parse position; advanced past the closing ] on success. + /// A slice of containing the type name, without the brackets. + /// if a non-empty bracketed type name was consumed. + internal static bool TryParseBracketedTypeName(ReadOnlySpan span, ref int pos, out ReadOnlySpan typeName) + { + typeName = default; + + // Required opening bracket. + if (pos >= span.Length || span[pos] != '[') + { + return false; + } + + pos++; + + // Type name: one or more chars that are not '[', ']', or ':'. + int typeNameStart = pos; + while (pos < span.Length && span[pos] != '[' && span[pos] != ']' && span[pos] != ':') + { + pos++; + } + + if (pos == typeNameStart) + { + return false; + } + + typeName = span.Slice(typeNameStart, pos - typeNameStart); + + // Required closing bracket. + if (pos >= span.Length || span[pos] != ']') + { + return false; + } + + pos++; + return true; + } +} diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/DiagnosticAnalyzerState.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/DiagnosticAnalyzerState.cs deleted file mode 100644 index 5a7812dfb..000000000 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/DiagnosticAnalyzerState.cs +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using System; - using System.Collections.Concurrent; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Linq; - using System.Runtime.CompilerServices; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - - /// - /// A class for our analyzers that provide per-compilation caching by way of its private fields - /// to support common utility methods. - /// - internal abstract class DiagnosticAnalyzerState - { - private const string GetAwaiterMethodName = nameof(Task.GetAwaiter); - - private readonly ConcurrentDictionary customAwaitableTypes = new ConcurrentDictionary(); - - internal bool IsAwaitableType(ITypeSymbol? typeSymbol, Compilation compilation, CancellationToken cancellationToken) - { - if (typeSymbol is null) - { - return false; - } - - if (!this.customAwaitableTypes.TryGetValue(typeSymbol, out bool isAwaitable)) - { - IMethodSymbol? getAwaiterMethod = typeSymbol.GetMembers(nameof(Task.GetAwaiter)).OfType().FirstOrDefault(m => m.Parameters.IsEmpty); - if (getAwaiterMethod is object) - { - isAwaitable = ConformsToAwaiterPattern(getAwaiterMethod.ReturnType); - } - else - { - IEnumerable? awaitableTypesFromThisAssembly = from candidateAwaiterMethod in compilation.GetSymbolsWithName(m => m == GetAwaiterMethodName, SymbolFilter.Member, cancellationToken).OfType() - where candidateAwaiterMethod.IsExtensionMethod && !candidateAwaiterMethod.Parameters.IsEmpty - where ConformsToAwaiterPattern(candidateAwaiterMethod.ReturnType) - select candidateAwaiterMethod.Parameters[0].Type; - IEnumerable? awaitableTypesPerAssembly = from assembly in compilation.Assembly.Modules.First().ReferencedAssemblySymbols - from awaitableType in GetAwaitableTypes(assembly) - select awaitableType; - isAwaitable = awaitableTypesFromThisAssembly.Concat(awaitableTypesPerAssembly).Contains(typeSymbol); - } - - this.customAwaitableTypes.TryAdd(typeSymbol, isAwaitable); - } - - return isAwaitable; - } - - private static bool ConformsToAwaiterPattern(ITypeSymbol typeSymbol) - { - if (typeSymbol is null) - { - return false; - } - - var hasGetResultMethod = false; - var hasOnCompletedMethod = false; - var hasIsCompletedProperty = false; - - foreach (ISymbol? member in typeSymbol.GetMembers()) - { - hasGetResultMethod |= member.Name == nameof(TaskAwaiter.GetResult) && member is IMethodSymbol m && m.Parameters.IsEmpty; - hasOnCompletedMethod |= member.Name == nameof(TaskAwaiter.OnCompleted) && member is IMethodSymbol; - hasIsCompletedProperty |= member.Name == nameof(TaskAwaiter.IsCompleted) && member is IPropertySymbol; - - if (hasGetResultMethod && hasOnCompletedMethod && hasIsCompletedProperty) - { - return true; - } - } - - return false; - } - - private static IEnumerable GetAwaitableTypes(IAssemblySymbol assembly) - { - if (assembly is null) - { - throw new ArgumentNullException(nameof(assembly)); - } - - if (!assembly.MightContainExtensionMethods) - { - return Enumerable.Empty(); - } - - return GetAwaitableTypes(assembly.GlobalNamespace); - } - - private static IEnumerable GetAwaitableTypes(INamespaceOrTypeSymbol namespaceOrTypeSymbol) - { - if (namespaceOrTypeSymbol is null || namespaceOrTypeSymbol.DeclaredAccessibility != Accessibility.Public) - { - yield break; - } - - foreach (ISymbol? member in namespaceOrTypeSymbol.GetMembers()) - { - switch (member) - { - case INamespaceOrTypeSymbol nsOrType: - foreach (ITypeSymbol? nested in GetAwaitableTypes(nsOrType)) - { - yield return nested; - } - - break; - case IMethodSymbol method: - if (method.DeclaredAccessibility == Accessibility.Public && - method.IsExtensionMethod && - method.Name == GetAwaiterMethodName && - !method.Parameters.IsEmpty && - ConformsToAwaiterPattern(method.ReturnType)) - { - yield return method.Parameters[0].Type; - } - - break; - } - } - } - } -} diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/LanguageUtils.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/LanguageUtils.cs index 03692919c..18cb25ee1 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/LanguageUtils.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/LanguageUtils.cs @@ -1,20 +1,21 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +public abstract class LanguageUtils { - using System.Threading; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Operations; + public abstract Location? GetLocationOfBaseTypeName(INamedTypeSymbol symbol, INamedTypeSymbol baseType, Compilation compilation, CancellationToken cancellationToken); - internal abstract class LanguageUtils - { - internal abstract Location? GetLocationOfBaseTypeName(INamedTypeSymbol symbol, INamedTypeSymbol baseType, Compilation compilation, CancellationToken cancellationToken); + public abstract SyntaxNode IsolateMethodName(IInvocationOperation invocation); - internal abstract SyntaxNode IsolateMethodName(IInvocationOperation invocation); + public abstract SyntaxNode IsolateMethodName(IObjectCreationOperation objectCreation); - internal abstract SyntaxNode IsolateMethodName(IObjectCreationOperation objectCreation); + public abstract bool MethodReturnsNullableReferenceType(IMethodSymbol method); - internal abstract bool MethodReturnsNullableReferenceType(IMethodSymbol method); - } + public abstract bool IsAsyncMethod(SyntaxNode syntaxNode); } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/Microsoft.VisualStudio.Threading.Analyzers.csproj b/src/Microsoft.VisualStudio.Threading.Analyzers/Microsoft.VisualStudio.Threading.Analyzers.csproj index a6c688ef0..c2e860993 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/Microsoft.VisualStudio.Threading.Analyzers.csproj +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/Microsoft.VisualStudio.Threading.Analyzers.csproj @@ -1,34 +1,18 @@  + - netstandard1.3 + netstandard2.0 + true Microsoft.VisualStudio.Threading.Analyzers.Only false + $(NoWarn);CS1591 - - True - True - Strings.resx - + - - ResXFileCodeGenerator - Strings.Designer.cs - - - Strings.resx - - - - - - - - - - - - + + + diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/Namespaces.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/Namespaces.cs index 5aa3b2786..d1d8492b2 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/Namespaces.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/Namespaces.cs @@ -1,84 +1,81 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using System.Collections.Generic; +namespace Microsoft.VisualStudio.Threading.Analyzers; - internal static class Namespaces - { - internal static readonly IReadOnlyList System = new[] - { - nameof(System), - }; +public static class Namespaces +{ + public static readonly ImmutableArray System = + [ + nameof(System), + ]; - internal static readonly IReadOnlyList SystemCollectionsGeneric = new[] - { - nameof(System), - nameof(global::System.Collections), - nameof(global::System.Collections.Generic), - }; + public static readonly ImmutableArray SystemCollectionsGeneric = + [ + nameof(System), + nameof(global::System.Collections), + nameof(global::System.Collections.Generic), + ]; - internal static readonly IReadOnlyList SystemThreading = new[] - { - nameof(System), - nameof(global::System.Threading), - }; + public static readonly ImmutableArray SystemThreading = + [ + nameof(System), + nameof(global::System.Threading), + ]; - internal static readonly IReadOnlyList SystemDiagnostics = new[] - { - nameof(System), - nameof(global::System.Diagnostics), - }; + public static readonly ImmutableArray SystemDiagnostics = + [ + nameof(System), + nameof(global::System.Diagnostics), + ]; - internal static readonly IReadOnlyList SystemThreadingTasks = new[] - { - nameof(System), - nameof(global::System.Threading), - nameof(global::System.Threading.Tasks), - }; + public static readonly ImmutableArray SystemThreadingTasks = + [ + nameof(System), + nameof(global::System.Threading), + nameof(global::System.Threading.Tasks), + ]; - internal static readonly IReadOnlyList SystemRuntimeCompilerServices = new[] - { - nameof(System), - nameof(global::System.Runtime), - nameof(global::System.Runtime.CompilerServices), - }; + public static readonly ImmutableArray SystemRuntimeCompilerServices = + [ + nameof(System), + nameof(global::System.Runtime), + nameof(global::System.Runtime.CompilerServices), + ]; - internal static readonly IReadOnlyList SystemRuntimeInteropServices = new[] - { - nameof(System), - nameof(global::System.Runtime), - nameof(global::System.Runtime.InteropServices), - }; + public static readonly ImmutableArray SystemRuntimeInteropServices = + [ + nameof(System), + nameof(global::System.Runtime), + nameof(global::System.Runtime.InteropServices), + ]; - internal static readonly IReadOnlyList SystemWindowsThreading = new[] - { - nameof(System), - nameof(global::System.Windows), - "Threading", - }; + public static readonly ImmutableArray SystemWindowsThreading = + [ + nameof(System), + nameof(global::System.Windows), + "Threading", + ]; - internal static readonly IReadOnlyList MicrosoftVisualStudioThreading = new[] - { - "Microsoft", - "VisualStudio", - "Threading", - }; + public static readonly ImmutableArray MicrosoftVisualStudioThreading = + [ + "Microsoft", + "VisualStudio", + "Threading", + ]; - internal static readonly IReadOnlyList MicrosoftVisualStudioShell = new[] - { - "Microsoft", - "VisualStudio", - "Shell", - }; + public static readonly ImmutableArray MicrosoftVisualStudioShell = + [ + "Microsoft", + "VisualStudio", + "Shell", + ]; - internal static readonly IReadOnlyList MicrosoftVisualStudioShellInterop = new[] - { - "Microsoft", - "VisualStudio", - "Shell", - "Interop", - }; - } + public static readonly ImmutableArray MicrosoftVisualStudioShellInterop = + [ + "Microsoft", + "VisualStudio", + "Shell", + "Interop", + ]; } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/Strings.Designer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/Strings.Designer.cs deleted file mode 100644 index 99f4bbc2f..000000000 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/Strings.Designer.cs +++ /dev/null @@ -1,650 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Microsoft.VisualStudio.Threading.Analyzers { - using System; - using System.Reflection; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Strings { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Strings() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.Threading.Analyzers.Strings", typeof(Strings).GetTypeInfo().Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Await {0} instead. - /// - internal static string AwaitXInstead { - get { - return ResourceManager.GetString("AwaitXInstead", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The System.IAsyncDisposable interface is defined in the Microsoft.Bcl.AsyncInterfaces NuGet package.. - /// - internal static string SystemIAsyncDisposablePackageNote { - get { - return ResourceManager.GetString("SystemIAsyncDisposablePackageNote", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use await instead. - /// - internal static string UseAwaitInstead { - get { - return ResourceManager.GetString("UseAwaitInstead", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Await JoinableTaskFactory.SwitchToMainThreadAsync() to switch to the UI thread instead of APIs that can deadlock or require specifying a priority. - /// - internal static string VSTHRD001_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD001_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid legacy thread switching APIs. - /// - internal static string VSTHRD001_Title { - get { - return ResourceManager.GetString("VSTHRD001_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use await instead. - /// - internal static string VSTHRD002_CodeFix_Await_Title { - get { - return ResourceManager.GetString("VSTHRD002_CodeFix_Await_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Synchronously waiting on tasks or awaiters may cause deadlocks. Use await or JoinableTaskFactory.Run instead.. - /// - internal static string VSTHRD002_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD002_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid problematic synchronous waits. - /// - internal static string VSTHRD002_Title { - get { - return ResourceManager.GetString("VSTHRD002_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid awaiting or returning a Task representing work that was not started within your context as that can lead to deadlocks. - ///Start the work within this context, or use JoinableTaskFactory.RunAsync to start the task and await the returned JoinableTask instead.. - /// - internal static string VSTHRD003_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD003_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid awaiting foreign Tasks. - /// - internal static string VSTHRD003_Title { - get { - return ResourceManager.GetString("VSTHRD003_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Calls to JoinableTaskFactory.SwitchToMainThreadAsync() must be awaited. - /// - internal static string VSTHRD004_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD004_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Await SwitchToMainThreadAsync. - /// - internal static string VSTHRD004_Title { - get { - return ResourceManager.GetString("VSTHRD004_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Accessing "{0}" should only be done on the main thread. Await JoinableTaskFactory.SwitchToMainThreadAsync() first.. - /// - internal static string VSTHRD010_MessageFormat_Async { - get { - return ResourceManager.GetString("VSTHRD010_MessageFormat_Async", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Accessing "{0}" should only be done on the main thread. Call {1}() first.. - /// - internal static string VSTHRD010_MessageFormat_Sync { - get { - return ResourceManager.GetString("VSTHRD010_MessageFormat_Sync", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invoke single-threaded types on Main thread. - /// - internal static string VSTHRD010_Title { - get { - return ResourceManager.GetString("VSTHRD010_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Lazy<Task<T>>.Value can deadlock. Use AsyncLazy<T> instead.. - /// - internal static string VSTHRD011_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD011_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use AsyncLazy<T>. - /// - internal static string VSTHRD011_Title { - get { - return ResourceManager.GetString("VSTHRD011_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invoking or blocking on async code in a Lazy<T> value factory can deadlock. Use AsyncLazy<T> instead.. - /// - internal static string VSTHRD011b_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD011b_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Provide an instance of JoinableTaskFactory in this call (or another overload) to avoid deadlocks with the main thread. - /// - internal static string VSTHRD012_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD012_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Provide JoinableTaskFactory where allowed. - /// - internal static string VSTHRD012_Title { - get { - return ResourceManager.GetString("VSTHRD012_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Change return type to Task. - /// - internal static string VSTHRD100_CodeFix_Title { - get { - return ResourceManager.GetString("VSTHRD100_CodeFix_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid "async void" methods, because any exceptions not handled by the method will crash the process. - /// - internal static string VSTHRD100_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD100_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid async void methods. - /// - internal static string VSTHRD100_Title { - get { - return ResourceManager.GetString("VSTHRD100_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid using async lambda for a void returning delegate type, because any exceptions not handled by the delegate will crash the process. - /// - internal static string VSTHRD101_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD101_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid unsupported async delegates. - /// - internal static string VSTHRD101_Title { - get { - return ResourceManager.GetString("VSTHRD101_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Limit use of synchronously blocking method calls such as JoinableTaskFactory.Run or Task.Result to public entrypoint members where you must be synchronous. Using it for internal members can needlessly add synchronous frames between asynchronous frames, leading to threadpool exhaustion.. - /// - internal static string VSTHRD102_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD102_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Implement internal logic asynchronously. - /// - internal static string VSTHRD102_Title { - get { - return ResourceManager.GetString("VSTHRD102_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} synchronously blocks. Await {1} instead.. - /// - internal static string VSTHRD103_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD103_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} synchronously blocks. Use await instead.. - /// - internal static string VSTHRD103_MessageFormat_UseAwaitInstead { - get { - return ResourceManager.GetString("VSTHRD103_MessageFormat_UseAwaitInstead", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Call async methods when in an async method. - /// - internal static string VSTHRD103_Title { - get { - return ResourceManager.GetString("VSTHRD103_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Expose an async version of this method that does not synchronously block. Then simplify this method to call that async method within a JoinableTaskFactory.Run delegate.. - /// - internal static string VSTHRD104_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD104_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Offer async methods. - /// - internal static string VSTHRD104_Title { - get { - return ResourceManager.GetString("VSTHRD104_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid method overloads that assume TaskScheduler.Current. Use an overload that accepts a TaskScheduler and specify TaskScheduler.Default (or any other) explicitly.. - /// - internal static string VSTHRD105_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD105_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid method overloads that assume TaskScheduler.Current. - /// - internal static string VSTHRD105_Title { - get { - return ResourceManager.GetString("VSTHRD105_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AsyncEventHandler delegates should be invoked via the extension method "TplExtensions.InvokeAsync()" defined in Microsoft.VisualStudio.Threading assembly. - /// - internal static string VSTHRD106_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD106_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use InvokeAsync to raise async events. - /// - internal static string VSTHRD106_Title { - get { - return ResourceManager.GetString("VSTHRD106_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Await using expression. - /// - internal static string VSTHRD107_CodeFix_Title { - get { - return ResourceManager.GetString("VSTHRD107_CodeFix_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Missing await operator for "using" expression. - /// - internal static string VSTHRD107_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD107_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Await Task within using expression. - /// - internal static string VSTHRD107_Title { - get { - return ResourceManager.GetString("VSTHRD107_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Thread affinity checks should be unconditional. - /// - internal static string VSTHRD108_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD108_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Assert thread affinity unconditionally. - /// - internal static string VSTHRD108_Title { - get { - return ResourceManager.GetString("VSTHRD108_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid throwing when not on the main thread while in an async or Task-returning method. Switch to the thread required instead.. - /// - internal static string VSTHRD109_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD109_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Switch instead of assert in async methods. - /// - internal static string VSTHRD109_Title { - get { - return ResourceManager.GetString("VSTHRD109_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method.. - /// - internal static string VSTHRD110_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD110_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Observe result of async calls. - /// - internal static string VSTHRD110_Title { - get { - return ResourceManager.GetString("VSTHRD110_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Add .ConfigureAwait(false). - /// - internal static string VSTHRD111_CodeFix_False_Title { - get { - return ResourceManager.GetString("VSTHRD111_CodeFix_False_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Add .ConfigureAwait(true). - /// - internal static string VSTHRD111_CodeFix_True_Title { - get { - return ResourceManager.GetString("VSTHRD111_CodeFix_True_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Add .ConfigureAwait(bool) to your await expression. - /// - internal static string VSTHRD111_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD111_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use ConfigureAwait(bool). - /// - internal static string VSTHRD111_Title { - get { - return ResourceManager.GetString("VSTHRD111_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Add implementation of System.IAsyncDisposable.. - /// - internal static string VSTHRD112_CodeFix_Title { - get { - return ResourceManager.GetString("VSTHRD112_CodeFix_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Implement the System.IAsyncDisposable interface when implementing the obsolete Microsoft.VisualStudio.Threading.IAsyncDisposable interface. - /// - internal static string VSTHRD112_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD112_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Implement System.IAsyncDisposable. - /// - internal static string VSTHRD112_Title { - get { - return ResourceManager.GetString("VSTHRD112_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Add a check for System.IAsyncDisposable in the same code block that checks for Microsoft.VisualStudio.Threading.IAsyncDisposable that behaves similarly. - /// - internal static string VSTHRD113_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD113_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Check for System.IAsyncDisposable. - /// - internal static string VSTHRD113_Title { - get { - return ResourceManager.GetString("VSTHRD113_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use 'Task.CompletedTask' instead. - /// - internal static string VSTHRD114_CodeFix_CompletedTask { - get { - return ResourceManager.GetString("VSTHRD114_CodeFix_CompletedTask", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use 'Task.FromResult' instead. - /// - internal static string VSTHRD114_CodeFix_FromResult { - get { - return ResourceManager.GetString("VSTHRD114_CodeFix_FromResult", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid returning null from a Task-returning method. - /// - internal static string VSTHRD114_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD114_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid returning a null Task. - /// - internal static string VSTHRD114_Title { - get { - return ResourceManager.GetString("VSTHRD114_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use "Async" suffix in names of methods that return an awaitable type. - /// - internal static string VSTHRD200_AddAsync_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD200_AddAsync_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Rename to {0}. - /// - internal static string VSTHRD200_CodeFix_Title { - get { - return ResourceManager.GetString("VSTHRD200_CodeFix_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid "Async" suffix in names of methods that do not return an awaitable type. - /// - internal static string VSTHRD200_RemoveAsync_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD200_RemoveAsync_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use "Async" suffix for async methods. - /// - internal static string VSTHRD200_Title { - get { - return ResourceManager.GetString("VSTHRD200_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Call ThrowIfCancellationRequested(). - /// - internal static string VSTHRD201_CodeFix_Title { - get { - return ResourceManager.GetString("VSTHRD201_CodeFix_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Check for cancellation after calling SwitchToMainThreadAsync(CancellationToken).. - /// - internal static string VSTHRD201_MessageFormat { - get { - return ResourceManager.GetString("VSTHRD201_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Check cancellation after SwitchToMainThreadAsync. - /// - internal static string VSTHRD201_Title { - get { - return ResourceManager.GetString("VSTHRD201_Title", resourceCulture); - } - } - } -} diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/Strings.resx b/src/Microsoft.VisualStudio.Threading.Analyzers/Strings.resx index 236a96b45..97e248e4d 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/Strings.resx +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/Strings.resx @@ -271,7 +271,7 @@ Start the work within this context, or use JoinableTaskFactory.RunAsync to start Do not translate either of these. The first is a keyword, the second is a method name. - Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method. + Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method Observe result of async calls @@ -341,4 +341,16 @@ Start the work within this context, or use JoinableTaskFactory.RunAsync to start Use 'Task.FromResult' instead "Task.FromResult" should not be translated. + + Avoid creating JoinableTaskContext with null SynchronizationContext + + + Avoid creating JoinableTaskContext with 'null' as the value for the SynchronizationContext because behavior varies by the value of SynchronizationContext.Current + + + Specify 'SynchronizationContext.Current' explicitly + + + Use 'JoinableTaskContext.CreateNoOpContext' instead. + \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/Types.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/Types.cs index 628ac34e5..f6d24b8db 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/Types.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/Types.cs @@ -1,246 +1,249 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +namespace Microsoft.VisualStudio.Threading.Analyzers; + +/// +/// Identifiers used to identify various types so that we can avoid adding dependency only if absolutely needed. +/// For each predefine value here, please update the unit test to detect if values go out of sync with the real types they represent. +/// +public static class Types { - using System.Collections.Generic; + private const string VSThreadingNamespace = "Microsoft.VisualStudio.Threading"; - /// - /// Identifiers used to identify various types so that we can avoid adding dependency only if absolutely needed. - /// For each predefine value here, please update the unit test to detect if values go out of sync with the real types they represent. - /// - internal static class Types + public static class BclAsyncDisposable { - internal static class BclAsyncDisposable - { - internal const string FullName = "System.IAsyncDisposable"; + public const string FullName = "System.IAsyncDisposable"; - internal const string PackageId = "Microsoft.Bcl.AsyncInterfaces"; - } + public const string PackageId = "Microsoft.Bcl.AsyncInterfaces"; + } - internal static class IAsyncDisposable - { - internal const string FullName = "Microsoft.VisualStudio.Threading.IAsyncDisposable"; - } + public static class IAsyncDisposable + { + public const string FullName = "Microsoft.VisualStudio.Threading.IAsyncDisposable"; + } - internal static class AwaitExtensions - { - /// - /// The full name of the AwaitExtensions type. - /// - internal const string TypeName = "AwaitExtensions"; + public static class AwaitExtensions + { + /// + /// The full name of the AwaitExtensions type. + /// + public const string TypeName = "AwaitExtensions"; - /// - /// The name of the ConfigureAwaitRunInline method. - /// - internal const string ConfigureAwaitRunInline = "ConfigureAwaitRunInline"; + /// + /// The name of the ConfigureAwaitRunInline method. + /// + public const string ConfigureAwaitRunInline = "ConfigureAwaitRunInline"; - internal static readonly IReadOnlyList Namespace = Namespaces.MicrosoftVisualStudioThreading; - } + public static readonly ImmutableArray Namespace = Namespaces.MicrosoftVisualStudioThreading; + } + /// + /// Contains the names of types and members within TplExtensions. + /// + public static class TplExtensions + { /// - /// Contains the names of types and members within TplExtensions. + /// The full name of the TplExtensions type. /// - internal static class TplExtensions - { - /// - /// The full name of the TplExtensions type. - /// - internal const string TypeName = "TplExtensions"; - - /// - /// The name of the InvokeAsync method. - /// - internal const string InvokeAsync = "InvokeAsync"; - - /// - /// The name of the CompletedTask field. - /// - internal const string CompletedTask = "CompletedTask"; - - /// - /// The name of the CanceledTask field. - /// - internal const string CanceledTask = "CanceledTask"; - - /// - /// The name of the TrueTask field. - /// - internal const string TrueTask = "TrueTask"; - - /// - /// The name of the FalseTask field. - /// - internal const string FalseTask = "FalseTask"; - - internal static readonly IReadOnlyList Namespace = Namespaces.MicrosoftVisualStudioThreading; - } + public const string TypeName = "TplExtensions"; /// - /// Contains descriptors for the AsyncEventHandler type. + /// The name of the InvokeAsync method. /// - internal static class AsyncEventHandler - { - /// - /// The full name of the AsyncEventHandler type. - /// - internal const string TypeName = "AsyncEventHandler"; + public const string InvokeAsync = "InvokeAsync"; - internal static readonly IReadOnlyList Namespace = Namespaces.MicrosoftVisualStudioThreading; - } + /// + /// The name of the CompletedTask field. + /// + public const string CompletedTask = "CompletedTask"; - internal static class AsyncMethodBuilderAttribute - { - internal const string TypeName = nameof(System.Runtime.CompilerServices.AsyncMethodBuilderAttribute); + /// + /// The name of the CanceledTask field. + /// + public const string CanceledTask = "CanceledTask"; - internal static readonly IReadOnlyList Namespace = Namespaces.SystemRuntimeCompilerServices; - } + /// + /// The name of the TrueTask field. + /// + public const string TrueTask = "TrueTask"; /// - /// Contains descriptors for the JoinableTaskFactory type. + /// The name of the FalseTask field. /// - internal static class JoinableTaskFactory - { - internal const string TypeName = "JoinableTaskFactory"; + public const string FalseTask = "FalseTask"; - internal const string FullName = "Microsoft.VisualStudio.Threading." + TypeName; + public static readonly ImmutableArray Namespace = Namespaces.MicrosoftVisualStudioThreading; + } - /// - /// The name of the SwitchToMainThreadAsync method. - /// - internal const string SwitchToMainThreadAsync = "SwitchToMainThreadAsync"; + /// + /// Contains descriptors for the AsyncEventHandler type. + /// + public static class AsyncEventHandler + { + /// + /// The full name of the AsyncEventHandler type. + /// + public const string TypeName = "AsyncEventHandler"; - internal const string Run = "Run"; + public static readonly ImmutableArray Namespace = Namespaces.MicrosoftVisualStudioThreading; + } - internal const string RunAsync = "RunAsync"; + public static class AsyncMethodBuilderAttribute + { + public const string TypeName = nameof(System.Runtime.CompilerServices.AsyncMethodBuilderAttribute); - internal static readonly IReadOnlyList Namespace = Namespaces.MicrosoftVisualStudioThreading; - } + public static readonly ImmutableArray Namespace = Namespaces.SystemRuntimeCompilerServices; + } - /// - /// Contains descriptors for the JoinableTaskCollection type. - /// - internal static class JoinableTaskCollection - { - internal const string TypeName = "JoinableTaskCollection"; - } + /// + /// Contains descriptors for the JoinableTaskFactory type. + /// + public static class JoinableTaskFactory + { + public const string TypeName = "JoinableTaskFactory"; + + public const string FullName = "Microsoft.VisualStudio.Threading." + TypeName; /// - /// Contains descriptors for the JoinableTaskContext type. + /// The name of the SwitchToMainThreadAsync method. /// - internal static class JoinableTaskContext - { - internal const string TypeName = "JoinableTaskContext"; - } + public const string SwitchToMainThreadAsync = "SwitchToMainThreadAsync"; - internal static class JoinableTask - { - internal const string TypeName = "JoinableTask"; + public const string Run = "Run"; - internal const string Join = "Join"; + public const string RunAsync = "RunAsync"; - internal const string JoinAsync = "JoinAsync"; - } + public static readonly ImmutableArray Namespace = Namespaces.MicrosoftVisualStudioThreading; + } - internal static class SynchronizationContext - { - internal const string TypeName = nameof(System.Threading.SynchronizationContext); + /// + /// Contains descriptors for the JoinableTaskCollection type. + /// + public static class JoinableTaskCollection + { + public const string TypeName = "JoinableTaskCollection"; + } - internal const string Post = nameof(System.Threading.SynchronizationContext.Post); + /// + /// Contains descriptors for the JoinableTaskContext type. + /// + public static class JoinableTaskContext + { + public const string TypeName = "JoinableTaskContext"; - internal const string Send = nameof(System.Threading.SynchronizationContext.Send); - } + public const string FullName = $"{VSThreadingNamespace}.{TypeName}"; - internal static class ThreadHelper - { - internal const string TypeName = "ThreadHelper"; + public const string CreateNoOpContext = "CreateNoOpContext"; + } - internal const string Invoke = "Invoke"; + public static class JoinableTask + { + public const string TypeName = "JoinableTask"; - internal const string InvokeAsync = "InvokeAsync"; + public const string Join = "Join"; - internal const string BeginInvoke = "BeginInvoke"; + public const string JoinAsync = "JoinAsync"; + } - internal const string CheckAccess = "CheckAccess"; - } + public static class SynchronizationContext + { + public const string TypeName = nameof(System.Threading.SynchronizationContext); - internal static class Dispatcher - { - internal const string TypeName = "Dispatcher"; + public const string Post = nameof(System.Threading.SynchronizationContext.Post); - internal const string Invoke = "Invoke"; + public const string Send = nameof(System.Threading.SynchronizationContext.Send); + } - internal const string BeginInvoke = "BeginInvoke"; + public static class ThreadHelper + { + public const string TypeName = "ThreadHelper"; - internal const string InvokeAsync = "InvokeAsync"; - } + public const string Invoke = "Invoke"; - internal static class Task - { - internal const string TypeName = nameof(System.Threading.Tasks.Task); + public const string InvokeAsync = "InvokeAsync"; - internal const string FullName = "System.Threading.Tasks." + TypeName; + public const string BeginInvoke = "BeginInvoke"; - internal const string CompletedTask = nameof(System.Threading.Tasks.Task.CompletedTask); + public const string CheckAccess = "CheckAccess"; + } - internal const string WhenAll = "WhenAll"; + public static class Dispatcher + { + public const string TypeName = "Dispatcher"; - internal static readonly IReadOnlyList Namespace = Namespaces.SystemThreadingTasks; - } + public const string Invoke = "Invoke"; - internal static class ConfiguredTaskAwaitable - { - internal const string TypeName = nameof(System.Runtime.CompilerServices.ConfiguredTaskAwaitable); + public const string BeginInvoke = "BeginInvoke"; - internal const string FullName = "System.Runtime.CompilerServices." + TypeName; + public const string InvokeAsync = "InvokeAsync"; + } - internal static readonly IReadOnlyList Namespace = Namespaces.SystemRuntimeCompilerServices; - } + public static class Task + { + public const string TypeName = nameof(System.Threading.Tasks.Task); - internal static class ValueTask - { - internal const string TypeName = nameof(ValueTask); + public const string FullName = "System.Threading.Tasks." + TypeName; - internal const string FullName = "System.Threading.Tasks." + TypeName; + public const string CompletedTask = nameof(System.Threading.Tasks.Task.CompletedTask); - internal static readonly IReadOnlyList Namespace = Namespaces.SystemThreadingTasks; - } + public const string WhenAll = "WhenAll"; - internal static class ConfiguredValueTaskAwaitable - { - internal const string TypeName = nameof(System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable); + public static readonly ImmutableArray Namespace = Namespaces.SystemThreadingTasks; + } - internal const string FullName = "System.Runtime.CompilerServices." + TypeName; + public static class ConfiguredTaskAwaitable + { + public const string TypeName = nameof(System.Runtime.CompilerServices.ConfiguredTaskAwaitable); - internal static readonly IReadOnlyList Namespace = Namespaces.SystemRuntimeCompilerServices; - } + public const string FullName = "System.Runtime.CompilerServices." + TypeName; - internal static class CoClassAttribute - { - internal const string TypeName = nameof(System.Runtime.InteropServices.CoClassAttribute); + public static readonly ImmutableArray Namespace = Namespaces.SystemRuntimeCompilerServices; + } - internal static readonly IReadOnlyList Namespace = Namespaces.SystemRuntimeInteropServices; - } + public static class ValueTask + { + public const string TypeName = nameof(ValueTask); + + public const string FullName = "System.Threading.Tasks." + TypeName; + + public static readonly ImmutableArray Namespace = Namespaces.SystemThreadingTasks; + } + + public static class ConfiguredValueTaskAwaitable + { + public const string TypeName = nameof(System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable); + + public const string FullName = "System.Runtime.CompilerServices." + TypeName; + + public static readonly ImmutableArray Namespace = Namespaces.SystemRuntimeCompilerServices; + } - internal static class ComImportAttribute - { - internal const string TypeName = nameof(System.Runtime.InteropServices.ComImportAttribute); + public static class CoClassAttribute + { + public const string TypeName = nameof(System.Runtime.InteropServices.CoClassAttribute); - internal static readonly IReadOnlyList Namespace = Namespaces.SystemRuntimeInteropServices; - } + public static readonly ImmutableArray Namespace = Namespaces.SystemRuntimeInteropServices; + } - internal static class InterfaceTypeAttribute - { - internal const string TypeName = nameof(System.Runtime.InteropServices.InterfaceTypeAttribute); + public static class ComImportAttribute + { + public const string TypeName = nameof(System.Runtime.InteropServices.ComImportAttribute); - internal static readonly IReadOnlyList Namespace = Namespaces.SystemRuntimeInteropServices; - } + public static readonly ImmutableArray Namespace = Namespaces.SystemRuntimeInteropServices; + } - internal static class TypeLibTypeAttribute - { - internal const string TypeName = "TypeLibTypeAttribute"; + public static class InterfaceTypeAttribute + { + public const string TypeName = nameof(System.Runtime.InteropServices.InterfaceTypeAttribute); + + public static readonly ImmutableArray Namespace = Namespaces.SystemRuntimeInteropServices; + } + + public static class TypeLibTypeAttribute + { + public const string TypeName = "TypeLibTypeAttribute"; - internal static readonly IReadOnlyList Namespace = Namespaces.SystemRuntimeInteropServices; - } + public static readonly ImmutableArray Namespace = Namespaces.SystemRuntimeInteropServices; } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/Usings.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/Usings.cs new file mode 100644 index 000000000..d30c9f88f --- /dev/null +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/Usings.cs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +global using System.Collections.Immutable; diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/Utils.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/Utils.cs index fff3d222c..9a2c376f4 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/Utils.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/Utils.cs @@ -1,729 +1,746 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +public static class Utils { - using System; - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - using System.Linq; - using System.Reflection; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; - using Microsoft.CodeAnalysis.Operations; - - internal static class Utils - { - internal static Action DebuggableWrapper(Action handler) - { - return ctxt => + public static Action DebuggableWrapper(Action handler) + { + return ctxt => + { + try { - try - { - handler(ctxt); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) when (LaunchDebuggerExceptionFilter()) - { - throw new Exception($"Analyzer failure while processing syntax at {ctxt.Node.SyntaxTree.FilePath}({ctxt.Node.GetLocation()?.GetLineSpan().StartLinePosition.Line + 1},{ctxt.Node.GetLocation()?.GetLineSpan().StartLinePosition.Character + 1}): {ex.GetType()} {ex.Message}. Syntax: {ctxt.Node}", ex); - } - }; - } + handler(ctxt); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) when (LaunchDebuggerExceptionFilter()) + { + throw new Exception($"Analyzer failure while processing syntax at {ctxt.Node.SyntaxTree.FilePath}({ctxt.Node.GetLocation()?.GetLineSpan().StartLinePosition.Line + 1},{ctxt.Node.GetLocation()?.GetLineSpan().StartLinePosition.Character + 1}): {ex.GetType()} {ex.Message}. Syntax: {ctxt.Node}", ex); + } + }; + } - internal static Action DebuggableWrapper(Action handler) + public static Action DebuggableWrapper(Action handler) + { + return ctxt => { - return ctxt => + try { - try - { - handler(ctxt); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) when (LaunchDebuggerExceptionFilter()) - { - throw new Exception($"Analyzer failure while processing symbol {ctxt.Symbol} at {ctxt.Symbol.Locations.FirstOrDefault()?.SourceTree?.FilePath}({ctxt.Symbol.Locations.FirstOrDefault()?.GetLineSpan().StartLinePosition.Line},{ctxt.Symbol.Locations.FirstOrDefault()?.GetLineSpan().StartLinePosition.Character}): {ex.GetType()} {ex.Message}", ex); - } - }; - } + handler(ctxt); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) when (LaunchDebuggerExceptionFilter()) + { + throw new Exception($"Analyzer failure while processing symbol {ctxt.Symbol} at {ctxt.Symbol.Locations.FirstOrDefault()?.SourceTree?.FilePath}({ctxt.Symbol.Locations.FirstOrDefault()?.GetLineSpan().StartLinePosition.Line},{ctxt.Symbol.Locations.FirstOrDefault()?.GetLineSpan().StartLinePosition.Character}): {ex.GetType()} {ex.Message}", ex); + } + }; + } - internal static Action DebuggableWrapper(Action handler) + public static Action DebuggableWrapper(Action handler) + { + return ctxt => { - return ctxt => + try { - try - { - handler(ctxt); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) when (LaunchDebuggerExceptionFilter()) - { - throw new Exception($"Analyzer failure while processing syntax at {ctxt.CodeBlock.SyntaxTree.FilePath}({ctxt.CodeBlock.GetLocation()?.GetLineSpan().StartLinePosition.Line + 1},{ctxt.CodeBlock.GetLocation()?.GetLineSpan().StartLinePosition.Character + 1}): {ex.GetType()} {ex.Message}. Syntax: {ctxt.CodeBlock}", ex); - } - }; - } + handler(ctxt); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) when (LaunchDebuggerExceptionFilter()) + { + throw new Exception($"Analyzer failure while processing syntax at {ctxt.CodeBlock.SyntaxTree.FilePath}({ctxt.CodeBlock.GetLocation()?.GetLineSpan().StartLinePosition.Line + 1},{ctxt.CodeBlock.GetLocation()?.GetLineSpan().StartLinePosition.Character + 1}): {ex.GetType()} {ex.Message}. Syntax: {ctxt.CodeBlock}", ex); + } + }; + } - internal static Action DebuggableWrapper(Action handler) + public static Action DebuggableWrapper(Action handler) + { + return ctxt => { - return ctxt => + try { - try - { - handler(ctxt); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) when (LaunchDebuggerExceptionFilter()) - { - throw new Exception($"Analyzer failure while processing syntax at {ctxt.Operation.Syntax.SyntaxTree.FilePath}({ctxt.Operation.Syntax.GetLocation()?.GetLineSpan().StartLinePosition.Line + 1},{ctxt.Operation.Syntax.GetLocation()?.GetLineSpan().StartLinePosition.Character + 1}): {ex.GetType()} {ex.Message}. Syntax: {ctxt.Operation.Syntax}", ex); - } - }; - } + handler(ctxt); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) when (LaunchDebuggerExceptionFilter()) + { + throw new Exception($"Analyzer failure while processing syntax at {ctxt.Operation.Syntax.SyntaxTree.FilePath}({ctxt.Operation.Syntax.GetLocation()?.GetLineSpan().StartLinePosition.Line + 1},{ctxt.Operation.Syntax.GetLocation()?.GetLineSpan().StartLinePosition.Character + 1}): {ex.GetType()} {ex.Message}. Syntax: {ctxt.Operation.Syntax}", ex); + } + }; + } - internal static Action DebuggableWrapper(Action handler) + public static Action DebuggableWrapper(Action handler) + { + return ctxt => { - return ctxt => + try { - try - { - handler(ctxt); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) when (LaunchDebuggerExceptionFilter()) + handler(ctxt); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) when (LaunchDebuggerExceptionFilter()) + { + var messageBuilder = new StringBuilder(); + messageBuilder.Append("Analyzer failure while processing syntax(es) at "); + + for (int i = 0; i < ctxt.OperationBlocks.Length; i++) { - var messageBuilder = new StringBuilder(); - messageBuilder.Append("Analyzer failure while processing syntax(es) at "); + IOperation? operation = ctxt.OperationBlocks[i]; + FileLinePositionSpan? lineSpan = operation.Syntax.GetLocation()?.GetLineSpan(); - for (int i = 0; i < ctxt.OperationBlocks.Length; i++) + if (i > 0) { - IOperation? operation = ctxt.OperationBlocks[i]; - FileLinePositionSpan? lineSpan = operation.Syntax.GetLocation()?.GetLineSpan(); - - if (i > 0) - { - messageBuilder.Append(", "); - } - - messageBuilder.Append($"{operation.Syntax.SyntaxTree.FilePath}({lineSpan?.StartLinePosition.Line + 1},{lineSpan?.StartLinePosition.Character + 1}). Syntax: {operation.Syntax}."); + messageBuilder.Append(", "); } - messageBuilder.Append($". {ex.GetType()} {ex.Message}"); - - throw new Exception(messageBuilder.ToString(), ex); + messageBuilder.Append($"{operation.Syntax.SyntaxTree.FilePath}({lineSpan?.StartLinePosition.Line + 1},{lineSpan?.StartLinePosition.Character + 1}). Syntax: {operation.Syntax}."); } - }; - } - /// - /// Gets a semantic model for the given . - /// - internal static bool TryGetNewOrExistingSemanticModel(this SyntaxNodeAnalysisContext context, SyntaxTree syntaxTree, [NotNullWhen(true)] out SemanticModel? semanticModel) - { - // Avoid calling GetSemanticModel unless we need it since it's much more expensive to create a new one than to reuse one. - semanticModel = - context.Node.SyntaxTree == syntaxTree ? context.SemanticModel : - context.Compilation.ContainsSyntaxTree(syntaxTree) ? context.Compilation.GetSemanticModel(syntaxTree) : - null; - return semanticModel is object; - } + messageBuilder.Append($". {ex.GetType()} {ex.Message}"); - internal static bool IsEqualToOrDerivedFrom(ITypeSymbol? type, ITypeSymbol expectedType) - { - return EqualityComparer.Default.Equals(type?.OriginalDefinition, expectedType) || IsDerivedFrom(type, expectedType); - } - - internal static bool IsDerivedFrom(ITypeSymbol? type, ITypeSymbol expectedType) - { - type = type?.BaseType; - while (type is object) - { - if (EqualityComparer.Default.Equals(type.OriginalDefinition, expectedType)) - { - return true; - } - - type = type.BaseType; + throw new Exception(messageBuilder.ToString(), ex); } + }; + } - return false; - } + /// + /// Gets a semantic model for the given . + /// + public static bool TryGetNewOrExistingSemanticModel(this SyntaxNodeAnalysisContext context, SyntaxTree syntaxTree, [NotNullWhen(true)] out SemanticModel? semanticModel) + { + // Avoid calling GetSemanticModel unless we need it since it's much more expensive to create a new one than to reuse one. + semanticModel = + context.Node.SyntaxTree == syntaxTree ? context.SemanticModel : + context.Compilation.ContainsSyntaxTree(syntaxTree) ? context.Compilation.GetSemanticModel(syntaxTree) : + null; + return semanticModel is object; + } - /// - /// Resolve the type from the given symbol if possible. - /// For instance, if the symbol represents a property in a class, this method will return the type of that property. - /// - /// The input symbol. - /// The type represented by the input symbol; or null if could not figure out the type. - internal static ITypeSymbol? ResolveTypeFromSymbol(ISymbol symbol) + public static bool IsEqualToOrDerivedFrom(ITypeSymbol? type, ITypeSymbol expectedType) + { + return EqualityComparer.Default.Equals(type?.OriginalDefinition, expectedType) || IsDerivedFrom(type, expectedType); + } + + public static bool IsDerivedFrom(ITypeSymbol? type, ITypeSymbol expectedType) + { + type = type?.BaseType; + while (type is object) { - ITypeSymbol? type = null; - switch (symbol?.Kind) + if (EqualityComparer.Default.Equals(type.OriginalDefinition, expectedType)) { - case SymbolKind.Local: - type = ((ILocalSymbol)symbol).Type; - break; - - case SymbolKind.Field: - type = ((IFieldSymbol)symbol).Type; - break; - - case SymbolKind.Parameter: - type = ((IParameterSymbol)symbol).Type; - break; - - case SymbolKind.Property: - type = ((IPropertySymbol)symbol).Type; - break; - - case SymbolKind.Method: - var method = (IMethodSymbol)symbol; - type = method.MethodKind == MethodKind.Constructor ? method.ContainingType : method.ReturnType; - break; - - case SymbolKind.Event: - type = ((IEventSymbol)symbol).Type; - break; + return true; } - return type; + type = type.BaseType; } - /// - /// Tests whether a symbol belongs to a given namespace. - /// - /// The symbol whose namespace membership is being tested. - /// A sequence of namespaces from global to most precise. For example: [System, Threading, Tasks]. - /// true if the symbol belongs to the given namespace; otherwise false. - internal static bool BelongsToNamespace(this ISymbol symbol, IReadOnlyList namespaces) + return false; + } + + /// + /// Resolve the type from the given symbol if possible. + /// For instance, if the symbol represents a property in a class, this method will return the type of that property. + /// + /// The input symbol. + /// The type represented by the input symbol; or if could not figure out the type. + public static ITypeSymbol? ResolveTypeFromSymbol(ISymbol symbol) + { + ITypeSymbol? type = null; + switch (symbol?.Kind) { - if (namespaces is null) - { - throw new ArgumentNullException(nameof(namespaces)); - } + case SymbolKind.Local: + type = ((ILocalSymbol)symbol).Type; + break; - if (symbol is null) - { - return false; - } + case SymbolKind.Field: + type = ((IFieldSymbol)symbol).Type; + break; - INamespaceSymbol currentNamespace = symbol.ContainingNamespace; - for (int i = namespaces.Count - 1; i >= 0; i--) - { - if (currentNamespace?.Name != namespaces[i]) - { - return false; - } + case SymbolKind.Parameter: + type = ((IParameterSymbol)symbol).Type; + break; - currentNamespace = currentNamespace.ContainingNamespace; - } + case SymbolKind.Property: + type = ((IPropertySymbol)symbol).Type; + break; - return currentNamespace?.IsGlobalNamespace ?? false; + case SymbolKind.Method: + var method = (IMethodSymbol)symbol; + type = method.MethodKind == MethodKind.Constructor ? method.ContainingType : method.ReturnType; + break; + + case SymbolKind.Event: + type = ((IEventSymbol)symbol).Type; + break; } - internal static IBlockOperation? GetContainingFunctionBlock(IOperation operation) - { - IOperation? previousAncestor = operation; - IOperation? ancestor = previousAncestor; - do - { - if (previousAncestor != ancestor) - { - previousAncestor = ancestor; - } + return type; + } - ancestor = ancestor.Parent; - } - while (ancestor is object && ancestor.Kind != OperationKind.MethodBodyOperation && ancestor.Kind != OperationKind.AnonymousFunction && - ancestor.Kind != OperationKind.LocalFunction); + /// + /// Tests whether a symbol belongs to a given namespace. + /// + /// The symbol whose namespace membership is being tested. + /// A sequence of namespaces from global to most precise. For example: [System, Threading, Tasks]. + /// if the symbol belongs to the given namespace; otherwise . + public static bool BelongsToNamespace(this ISymbol symbol, IReadOnlyList namespaces) + { + if (namespaces is null) + { + throw new ArgumentNullException(nameof(namespaces)); + } - return previousAncestor as IBlockOperation; + if (symbol is null) + { + return false; } - internal static ISymbol GetContainingFunction(IOperation operation, ISymbol operationBlockContainingSymbol) + INamespaceSymbol currentNamespace = symbol.ContainingNamespace; + for (int i = namespaces.Count - 1; i >= 0; i--) { - for (IOperation? current = operation; current is object; current = current.Parent) + if (currentNamespace?.Name != namespaces[i]) { - if (current.Kind == OperationKind.AnonymousFunction) - { - return ((IAnonymousFunctionOperation)current).Symbol; - } - else if (current.Kind == OperationKind.LocalFunction) - { - return ((ILocalFunctionOperation)current).Symbol; - } + return false; } - return operationBlockContainingSymbol; + currentNamespace = currentNamespace.ContainingNamespace; } - internal static bool HasAsyncCompatibleReturnType([NotNullWhen(true)] this IMethodSymbol? methodSymbol) => IsAsyncCompatibleReturnType(methodSymbol?.ReturnType); + return currentNamespace?.IsGlobalNamespace ?? false; + } - /// - /// Determines whether a type could be used with the async modifier as a method return type. - /// - /// The type returned from a method. - /// true if the type can be returned from an async method. - /// - /// This is not the same thing as being an *awaitable* type, which is a much lower bar. Any type can be made awaitable by offering a GetAwaiter method - /// that follows the proper pattern. But being an async-compatible type in this sense is a type that can be returned from a method carrying the async keyword modifier, - /// in that the type is either the special Task type, or offers an async method builder of its own. - /// - internal static bool IsAsyncCompatibleReturnType([NotNullWhen(true)] this ITypeSymbol? typeSymbol) + public static IBlockOperation? GetContainingFunctionBlock(IOperation operation) + { + IOperation? previousAncestor = operation; + IOperation? ancestor = previousAncestor; + do { - if (typeSymbol is null) + if (previousAncestor != ancestor) { - return false; + previousAncestor = ancestor; } - // ValueTask and ValueTask have the AsyncMethodBuilderAttribute. - return (typeSymbol.Name == nameof(Task) && typeSymbol.BelongsToNamespace(Namespaces.SystemThreadingTasks)) - || IsIAsyncEnumerable(typeSymbol) || typeSymbol.AllInterfaces.Any(IsIAsyncEnumerable) - || typeSymbol.GetAttributes().Any(ad => ad.AttributeClass?.Name == Types.AsyncMethodBuilderAttribute.TypeName && ad.AttributeClass.BelongsToNamespace(Types.AsyncMethodBuilderAttribute.Namespace)); + ancestor = ancestor.Parent; + } + while (ancestor is object && ancestor.Kind != OperationKind.MethodBodyOperation && ancestor.Kind != OperationKind.AnonymousFunction && + ancestor.Kind != OperationKind.LocalFunction); - static bool IsIAsyncEnumerable(ITypeSymbol symbol) - => symbol.Name == "IAsyncEnumerable" // TODO: Use nameof(IAsyncEnumerable) after upgrade to netstandard2.1 - && symbol.BelongsToNamespace(Namespaces.SystemCollectionsGeneric); + return previousAncestor as IBlockOperation; + } + + public static ISymbol GetContainingFunction(IOperation operation, ISymbol operationBlockContainingSymbol) + { + for (IOperation? current = operation; current is object; current = current.Parent) + { + if (current.Kind == OperationKind.AnonymousFunction) + { + return ((IAnonymousFunctionOperation)current).Symbol; + } + else if (current.Kind == OperationKind.LocalFunction) + { + return ((ILocalFunctionOperation)current).Symbol; + } } - internal static bool IsLazyOfT([NotNullWhen(true)] INamedTypeSymbol? constructedType) + return operationBlockContainingSymbol; + } + + public static bool HasAsyncCompatibleReturnType([NotNullWhen(true)] this IMethodSymbol? methodSymbol) => IsAsyncCompatibleReturnType(methodSymbol?.ReturnType); + + /// + /// Determines whether a type could be used with the async modifier as a method return type. + /// + /// The type returned from a method. + /// if the type can be returned from an async method. + /// + /// This is not the same thing as being an *awaitable* type, which is a much lower bar. Any type can be made awaitable by offering a GetAwaiter method + /// that follows the proper pattern. But being an async-compatible type in this sense is a type that can be returned from a method carrying the async keyword modifier, + /// in that the type is either the special Task type, or offers an async method builder of its own. + /// + public static bool IsAsyncCompatibleReturnType([NotNullWhen(true)] this ITypeSymbol? typeSymbol) + { + if (typeSymbol is null) { - return constructedType is object - && constructedType.ContainingNamespace?.Name == nameof(System) - && (constructedType.ContainingNamespace.ContainingNamespace?.IsGlobalNamespace ?? false) - && constructedType.Name == nameof(Lazy) - && constructedType.Arity > 0; // could be Lazy or Lazy + return false; } - internal static bool IsTask([NotNullWhen(true)] ITypeSymbol? typeSymbol) => typeSymbol?.Name == nameof(Task) && typeSymbol.BelongsToNamespace(Namespaces.SystemThreadingTasks); + // ValueTask and ValueTask have the AsyncMethodBuilderAttribute. + return (typeSymbol.Name == nameof(Task) && typeSymbol.BelongsToNamespace(Namespaces.SystemThreadingTasks)) + || IsIAsyncEnumerableOrEnumerator(typeSymbol) || typeSymbol.AllInterfaces.Any(IsIAsyncEnumerableOrEnumerator) + || typeSymbol.GetAttributes().Any(ad => ad.AttributeClass?.Name == Types.AsyncMethodBuilderAttribute.TypeName && ad.AttributeClass.BelongsToNamespace(Types.AsyncMethodBuilderAttribute.Namespace)); + + static bool IsIAsyncEnumerableOrEnumerator(ITypeSymbol symbol) + => (symbol.Name == "IAsyncEnumerable" || symbol.Name == "IAsyncEnumerator") // TODO: Use nameof after upgrade to netstandard2.1 + && symbol.BelongsToNamespace(Namespaces.SystemCollectionsGeneric); + } + + public static bool IsLazyOfT([NotNullWhen(true)] INamedTypeSymbol? constructedType) + { + return constructedType is object + && constructedType.ContainingNamespace?.Name == nameof(System) + && (constructedType.ContainingNamespace.ContainingNamespace?.IsGlobalNamespace ?? false) + && constructedType.Name == nameof(Lazy) + && constructedType.Arity > 0; // could be Lazy or Lazy + } + + public static bool IsTask([NotNullWhen(true)] ITypeSymbol? typeSymbol) => typeSymbol?.Name == nameof(Task) && typeSymbol.BelongsToNamespace(Namespaces.SystemThreadingTasks); + + /// + /// Gets a value indicating whether a method is async or is ready to be async by having an async-compatible return type. + /// + /// + /// A method might be async but not have an async compatible return type if it returns void and is an "async void" method. + /// However, a non-async void method is *not* considered async ready and gets a false value returned from this method. + /// + public static bool IsAsyncReady(this IMethodSymbol methodSymbol) => methodSymbol.IsAsync || methodSymbol.HasAsyncCompatibleReturnType(); - /// - /// Gets a value indicating whether a method is async or is ready to be async by having an async-compatible return type. - /// - /// - /// A method might be async but not have an async compatible return type if it returns void and is an "async void" method. - /// However, a non-async void method is *not* considered async ready and gets a false value returned from this method. - /// - internal static bool IsAsyncReady(this IMethodSymbol methodSymbol) => methodSymbol.IsAsync || methodSymbol.HasAsyncCompatibleReturnType(); + public static bool HasAsyncAlternative(this IMethodSymbol methodSymbol, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return methodSymbol.ContainingType.GetMembers(methodSymbol.Name + VSTHRD200UseAsyncNamingConventionAnalyzer.MandatoryAsyncSuffix) + .Any(alt => IsXAtLeastAsPublicAsY(alt, methodSymbol)); + } - internal static bool HasAsyncAlternative(this IMethodSymbol methodSymbol, CancellationToken cancellationToken) + public static bool IsXAtLeastAsPublicAsY(ISymbol x, ISymbol y) + { + if (y.DeclaredAccessibility == x.DeclaredAccessibility || + x.DeclaredAccessibility == Accessibility.Public) { - cancellationToken.ThrowIfCancellationRequested(); - return methodSymbol.ContainingType.GetMembers(methodSymbol.Name + VSTHRD200UseAsyncNamingConventionAnalyzer.MandatoryAsyncSuffix) - .Any(alt => IsXAtLeastAsPublicAsY(alt, methodSymbol)); + return true; } - internal static bool IsXAtLeastAsPublicAsY(ISymbol x, ISymbol y) + switch (y.DeclaredAccessibility) { - if (y.DeclaredAccessibility == x.DeclaredAccessibility || - x.DeclaredAccessibility == Accessibility.Public) - { + case Accessibility.Private: return true; - } + case Accessibility.ProtectedAndInternal: + case Accessibility.Protected: + case Accessibility.Internal: + return x.DeclaredAccessibility == Accessibility.ProtectedOrInternal; + case Accessibility.ProtectedOrInternal: + case Accessibility.Public: + case Accessibility.NotApplicable: + default: + return false; + } + } - switch (y.DeclaredAccessibility) - { - case Accessibility.Private: - return true; - case Accessibility.ProtectedAndInternal: - case Accessibility.Protected: - case Accessibility.Internal: - return x.DeclaredAccessibility == Accessibility.ProtectedOrInternal; - case Accessibility.ProtectedOrInternal: - case Accessibility.Public: - case Accessibility.NotApplicable: - default: - return false; - } + /// + /// Determines whether a given symbol's declaration is visible outside the assembly + /// (and thus refactoring it may introduce breaking changes.) + /// + /// The symbol to be tested. + /// + /// if the symbol is a public type or member, + /// or a protected member inside a public type, + /// or an explicit interface implementation of a public interface; + /// otherwise . + /// + public static bool IsPublic([NotNullWhen(true)] ISymbol? symbol) + { + if (symbol is null) + { + return false; } - /// - /// Determines whether a given symbol's declaration is visible outside the assembly - /// (and thus refactoring it may introduce breaking changes.) - /// - /// The symbol to be tested. - /// - /// true if the symbol is a public type or member, - /// or a protected member inside a public type, - /// or an explicit interface implementation of a public interface; - /// otherwise false. - /// - internal static bool IsPublic([NotNullWhen(true)] ISymbol? symbol) - { - if (symbol is null) - { + if (symbol is INamespaceSymbol) + { + return true; + } + + // The only member that is public without saying so are explicit interface implementations; + // and only when the interfaces implemented are themselves public. + var methodSymbol = symbol as IMethodSymbol; + if (methodSymbol?.ExplicitInterfaceImplementations.Any(IsPublic) ?? false) + { + return true; + } + + switch (symbol.DeclaredAccessibility) + { + case Accessibility.Internal: + case Accessibility.Private: + case Accessibility.ProtectedAndInternal: return false; - } + case Accessibility.Protected: + case Accessibility.ProtectedOrInternal: + case Accessibility.Public: + return symbol.ContainingType is null || IsPublic(symbol.ContainingType); + case Accessibility.NotApplicable: + default: + return false; + } + } - if (symbol is INamespaceSymbol) - { - return true; - } + public static bool IsEntrypointMethod([NotNullWhen(true)] ISymbol? symbol, SemanticModel? semanticModel, CancellationToken cancellationToken) + { + return semanticModel?.Compilation is object && IsEntrypointMethod(symbol, semanticModel.Compilation, cancellationToken); + } - // The only member that is public without saying so are explicit interface implementations; - // and only when the interfaces implemented are themselves public. - var methodSymbol = symbol as IMethodSymbol; - if (methodSymbol?.ExplicitInterfaceImplementations.Any(IsPublic) ?? false) - { - return true; - } + public static bool IsEntrypointMethod([NotNullWhen(true)] ISymbol? symbol, Compilation compilation, CancellationToken cancellationToken) + { + return compilation.GetEntryPoint(cancellationToken)?.Equals(symbol, SymbolEqualityComparer.Default) ?? false; + } - switch (symbol.DeclaredAccessibility) - { - case Accessibility.Internal: - case Accessibility.Private: - case Accessibility.ProtectedAndInternal: - return false; - case Accessibility.Protected: - case Accessibility.ProtectedOrInternal: - case Accessibility.Public: - return symbol.ContainingType is null || IsPublic(symbol.ContainingType); - case Accessibility.NotApplicable: - default: - return false; - } - } + public static bool IsObsolete(this ISymbol symbol) + { + return symbol.GetAttributes().Any(a => a.AttributeClass?.Name == nameof(ObsoleteAttribute) && a.AttributeClass.BelongsToNamespace(Namespaces.System)); + } - internal static bool IsEntrypointMethod([NotNullWhen(true)] ISymbol? symbol, SemanticModel semanticModel, CancellationToken cancellationToken) + public static IEnumerable FindInterfacesImplemented(this ISymbol? symbol) + { + if (symbol is null) { - return semanticModel.Compilation is object && IsEntrypointMethod(symbol, semanticModel.Compilation, cancellationToken); + return Enumerable.Empty(); } - internal static bool IsEntrypointMethod([NotNullWhen(true)] ISymbol? symbol, Compilation compilation, CancellationToken cancellationToken) + IEnumerable? interfaceImplementations = from iface in symbol.ContainingType.AllInterfaces + from member in iface.GetMembers() + let implementingMember = symbol.ContainingType.FindImplementationForInterfaceMember(member) + where implementingMember?.Equals(symbol, SymbolEqualityComparer.Default) ?? false + select iface; + + return interfaceImplementations; + } + + public static string GetFullName(ISymbol symbol) + { + if (symbol is null) { - return compilation.GetEntryPoint(cancellationToken)?.Equals(symbol) ?? false; + throw new ArgumentNullException(nameof(symbol)); } - internal static bool IsObsolete(this ISymbol symbol) + var sb = new StringBuilder(); + sb.Append(symbol.Name); + while (symbol.ContainingType is object) { - return symbol.GetAttributes().Any(a => a.AttributeClass.Name == nameof(ObsoleteAttribute) && a.AttributeClass.BelongsToNamespace(Namespaces.System)); + sb.Insert(0, symbol.ContainingType.Name + "."); + symbol = symbol.ContainingType; } - internal static IEnumerable FindInterfacesImplemented(this ISymbol? symbol) + while (symbol.ContainingNamespace is object) { - if (symbol is null) + if (!string.IsNullOrEmpty(symbol.ContainingNamespace.Name)) { - return Enumerable.Empty(); + sb.Insert(0, symbol.ContainingNamespace.Name + "."); } - IEnumerable? interfaceImplementations = from iface in symbol.ContainingType.AllInterfaces - from member in iface.GetMembers() - let implementingMember = symbol.ContainingType.FindImplementationForInterfaceMember(member) - where implementingMember?.Equals(symbol) ?? false - select iface; - - return interfaceImplementations; + symbol = symbol.ContainingNamespace; } - internal static string GetFullName(ISymbol symbol) - { - if (symbol is null) - { - throw new ArgumentNullException(nameof(symbol)); - } + return sb.ToString(); + } - var sb = new StringBuilder(); - sb.Append(symbol.Name); - while (symbol.ContainingType is object) - { - sb.Insert(0, symbol.ContainingType.Name + "."); - symbol = symbol.ContainingType; - } + public static void Deconstruct(this Tuple tuple, out T1 item1, out T2 item2) + { + item1 = tuple.Item1; + item2 = tuple.Item2; + } - while (symbol.ContainingNamespace is object) - { - if (!string.IsNullOrEmpty(symbol.ContainingNamespace.Name)) - { - sb.Insert(0, symbol.ContainingNamespace.Name + "."); - } + public static void Deconstruct(this Tuple tuple, out T1 item1, out T2 item2, out T3 item3) + { + item1 = tuple.Item1; + item2 = tuple.Item2; + item3 = tuple.Item3; + } - symbol = symbol.ContainingNamespace; - } + public static void Deconstruct(this Tuple tuple, out T1 item1, out T2 item2, out T3 item3, out T4 item4) + { + item1 = tuple.Item1; + item2 = tuple.Item2; + item3 = tuple.Item3; + item4 = tuple.Item4; + } + + public static string GetHelpLink(string analyzerId) + { + return $"https://microsoft.github.io/vs-threading/analyzers/{analyzerId}.html"; + } - return sb.ToString(); + /// + /// Looks for a symbol that represents a near + /// some document location that might be consumed in some generated invocation. + /// + /// The semantic model of the document. + /// The position in the document that must have access to any candidate . + /// A token that represents lost interest in this inquiry. + /// Candidate symbols. + public static IEnumerable FindCancellationToken(SemanticModel? semanticModel, int positionForLookup, CancellationToken cancellationToken) + { + if (semanticModel is null) + { + return Enumerable.Empty(); } - internal static void Deconstruct(this Tuple tuple, out T1 item1, out T2 item2) + ISymbol? enclosingSymbol = semanticModel.GetEnclosingSymbol(positionForLookup, cancellationToken); + if (enclosingSymbol is null) { - item1 = tuple.Item1; - item2 = tuple.Item2; + return Enumerable.Empty(); } - internal static void Deconstruct(this Tuple tuple, out T1 item1, out T2 item2, out T3 item3) + IOrderedEnumerable? cancellationTokenSymbols = semanticModel.LookupSymbols(positionForLookup) + .Where(s => (s.IsStatic || !enclosingSymbol.IsStatic) && s.CanBeReferencedByName && IsSymbolTheRightType(s, nameof(CancellationToken), Namespaces.SystemThreading)) + .OrderBy(s => s.ContainingSymbol.Equals(enclosingSymbol, SymbolEqualityComparer.Default) ? 1 : s.ContainingType.Equals(enclosingSymbol.ContainingType, SymbolEqualityComparer.Default) ? 2 : 3); // prefer locality + return cancellationTokenSymbols; + } + + /// + /// Find a set of methods that match a given method's fully qualified name. + /// + /// The semantic model of the document that must be able to access the methods. + /// The fully-qualified name of the method. + /// An enumeration of method symbols with a matching name. + public static IEnumerable FindMethodGroup(SemanticModel? semanticModel, string methodAsString) + { + if (semanticModel is null) { - item1 = tuple.Item1; - item2 = tuple.Item2; - item3 = tuple.Item3; + return Enumerable.Empty(); } - internal static void Deconstruct(this Tuple tuple, out T1 item1, out T2 item2, out T3 item3, out T4 item4) + if (string.IsNullOrEmpty(methodAsString)) { - item1 = tuple.Item1; - item2 = tuple.Item2; - item3 = tuple.Item3; - item4 = tuple.Item4; + throw new ArgumentException("A non-empty value is required.", nameof(methodAsString)); } - internal static string GetHelpLink(string analyzerId) + (string? fullTypeName, string? methodName) = SplitOffLastElement(methodAsString); + if (fullTypeName is null) { - return $"https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/{analyzerId}.md"; + return Enumerable.Empty(); } - /// - /// Looks for a symbol that represents a near - /// some document location that might be consumed in some generated invocation. - /// - /// The semantic model of the document. - /// The position in the document that must have access to any candidate . - /// A token that represents lost interest in this inquiry. - /// Candidate symbols. - internal static IEnumerable? FindCancellationToken(SemanticModel semanticModel, int positionForLookup, CancellationToken cancellationToken) + INamedTypeSymbol? proposedType = semanticModel.Compilation.GetTypeByMetadataName(fullTypeName); + + return methodName is not null && proposedType is not null + ? proposedType.GetMembers(methodName).OfType() + : Enumerable.Empty(); + } + + /// + /// Find a set of methods that match a given method's fully qualified name. + /// + /// The semantic model of the document that must be able to access the methods. + /// The fully-qualified name of the method. + /// An enumeration of method symbols with a matching name. + public static IEnumerable FindMethodGroup(SemanticModel semanticModel, CommonInterest.QualifiedMember method) + { + if (semanticModel is null) { - if (semanticModel is null) - { - throw new ArgumentNullException(nameof(semanticModel)); - } + throw new ArgumentNullException(nameof(semanticModel)); + } - ISymbol? enclosingSymbol = semanticModel.GetEnclosingSymbol(positionForLookup, cancellationToken); - if (enclosingSymbol is null) - { - return null; - } + INamedTypeSymbol? proposedType = semanticModel.Compilation.GetTypeByMetadataName(method.ContainingType.ToString()); + return proposedType?.GetMembers(method.Name).OfType() ?? Enumerable.Empty(); + } - IOrderedEnumerable? cancellationTokenSymbols = semanticModel.LookupSymbols(positionForLookup) - .Where(s => (s.IsStatic || !enclosingSymbol.IsStatic) && s.CanBeReferencedByName && IsSymbolTheRightType(s, nameof(CancellationToken), Namespaces.SystemThreading)) - .OrderBy(s => s.ContainingSymbol.Equals(enclosingSymbol) ? 1 : s.ContainingType.Equals(enclosingSymbol.ContainingType) ? 2 : 3); // prefer locality - return cancellationTokenSymbols; + /// + /// Finds a local variable, field, property, or static member on another type + /// that is typed to return a value of a given type. + /// + /// The type of value required. + /// The semantic model of the document that must be able to access the value. + /// The position in the document where the value must be accessible. + /// A cancellation token. + /// An enumeration of symbols that can provide a value of the required type, together with a flag indicating whether they are accessible using "local" syntax (i.e. the symbol is a local variable or a field on the enclosing type). + public static IEnumerable> FindInstanceOf(INamedTypeSymbol typeSymbol, SemanticModel semanticModel, int positionForLookup, CancellationToken cancellationToken) + { + if (typeSymbol is null) + { + throw new ArgumentNullException(nameof(typeSymbol)); } - /// - /// Find a set of methods that match a given method's fully qualified name. - /// - /// The semantic model of the document that must be able to access the methods. - /// The fully-qualified name of the method. - /// An enumeration of method symbols with a matching name. - internal static IEnumerable FindMethodGroup(SemanticModel semanticModel, string methodAsString) + if (semanticModel is null) { - if (semanticModel is null) - { - throw new ArgumentNullException(nameof(semanticModel)); - } + throw new ArgumentNullException(nameof(semanticModel)); + } - if (string.IsNullOrEmpty(methodAsString)) - { - throw new ArgumentException("A non-empty value is required.", nameof(methodAsString)); - } + ISymbol? enclosingSymbol = semanticModel.GetEnclosingSymbol(positionForLookup, cancellationToken); - (string? fullTypeName, string? methodName) = SplitOffLastElement(methodAsString); - (string? ns, string? leafTypeName) = SplitOffLastElement(fullTypeName); - string[]? namespaces = ns?.Split('.'); - if (fullTypeName is null) + // Search fields on the declaring type. + // Consider local variables too, if they're captured in a closure from some surrounding code block + // such that they would presumably be initialized by the time the first statement in our own code block runs. +#pragma warning disable CA1508 // Avoid dead conditional code -- compiler bug. It's not dead code. + ITypeSymbol? enclosingTypeSymbol = enclosingSymbol as ITypeSymbol ?? enclosingSymbol?.ContainingType; +#pragma warning restore CA1508 // Avoid dead conditional code + if (enclosingTypeSymbol is object) + { + IEnumerable? candidateMembers = from symbol in semanticModel.LookupSymbols(positionForLookup, enclosingTypeSymbol) + where symbol.IsStatic || !enclosingSymbol!.IsStatic + where IsSymbolTheRightType(symbol, typeSymbol.Name, typeSymbol.ContainingNamespace) + select symbol; + foreach (ISymbol? candidate in candidateMembers) { - return Enumerable.Empty(); + yield return Tuple.Create(true, candidate); } - - INamedTypeSymbol? proposedType = semanticModel.Compilation.GetTypeByMetadataName(fullTypeName); - - return proposedType?.GetMembers(methodName).OfType() ?? Enumerable.Empty(); } - /// - /// Find a set of methods that match a given method's fully qualified name. - /// - /// The semantic model of the document that must be able to access the methods. - /// The fully-qualified name of the method. - /// An enumeration of method symbols with a matching name. - internal static IEnumerable FindMethodGroup(SemanticModel semanticModel, CommonInterest.QualifiedMember method) + // Find static fields/properties that return the matching type from other public, non-generic types. + IEnumerable? candidateStatics = from offering in semanticModel.LookupStaticMembers(positionForLookup).OfType() + from symbol in offering.GetMembers() + where symbol.IsStatic && symbol.CanBeReferencedByName && IsSymbolTheRightType(symbol, typeSymbol.Name, typeSymbol.ContainingNamespace) + select symbol; + foreach (ISymbol? candidate in candidateStatics) { - if (semanticModel is null) - { - throw new ArgumentNullException(nameof(semanticModel)); - } + yield return Tuple.Create(false, candidate); + } + } - INamedTypeSymbol? proposedType = semanticModel.Compilation.GetTypeByMetadataName(method.ContainingType.ToString()); - return proposedType?.GetMembers(method.Name).OfType() ?? Enumerable.Empty(); + public static T? FirstAncestor(this SyntaxNode startingNode, IReadOnlyCollection doNotPassNodeTypes) + where T : SyntaxNode + { + if (doNotPassNodeTypes is null) + { + throw new ArgumentNullException(nameof(doNotPassNodeTypes)); } - /// - /// Finds a local variable, field, property, or static member on another type - /// that is typed to return a value of a given type. - /// - /// The type of value required. - /// The semantic model of the document that must be able to access the value. - /// The position in the document where the value must be accessible. - /// A cancellation token. - /// An enumeration of symbols that can provide a value of the required type, together with a flag indicating whether they are accessible using "local" syntax (i.e. the symbol is a local variable or a field on the enclosing type). - internal static IEnumerable> FindInstanceOf(INamedTypeSymbol typeSymbol, SemanticModel semanticModel, int positionForLookup, CancellationToken cancellationToken) + SyntaxNode? syntaxNode = startingNode; + while (syntaxNode is object) { - if (typeSymbol is null) + if (syntaxNode is T result) { - throw new ArgumentNullException(nameof(typeSymbol)); + return result; } - if (semanticModel is null) + if (doNotPassNodeTypes.Any(disallowed => disallowed.GetTypeInfo().IsAssignableFrom(syntaxNode.GetType().GetTypeInfo()))) { - throw new ArgumentNullException(nameof(semanticModel)); + return default(T); } - ISymbol? enclosingSymbol = semanticModel.GetEnclosingSymbol(positionForLookup, cancellationToken); + syntaxNode = syntaxNode.Parent; + } - // Search fields on the declaring type. - // Consider local variables too, if they're captured in a closure from some surrounding code block - // such that they would presumably be initialized by the time the first statement in our own code block runs. - ITypeSymbol enclosingTypeSymbol = enclosingSymbol as ITypeSymbol ?? enclosingSymbol.ContainingType; - if (enclosingTypeSymbol is object) - { - IEnumerable? candidateMembers = from symbol in semanticModel.LookupSymbols(positionForLookup, enclosingTypeSymbol) - where symbol.IsStatic || !enclosingSymbol.IsStatic - where IsSymbolTheRightType(symbol, typeSymbol.Name, typeSymbol.ContainingNamespace) - select symbol; - foreach (ISymbol? candidate in candidateMembers) - { - yield return Tuple.Create(true, candidate); - } - } + return default(T); + } - // Find static fields/properties that return the matching type from other public, non-generic types. - IEnumerable? candidateStatics = from offering in semanticModel.LookupStaticMembers(positionForLookup).OfType() - from symbol in offering.GetMembers() - where symbol.IsStatic && symbol.CanBeReferencedByName && IsSymbolTheRightType(symbol, typeSymbol.Name, typeSymbol.ContainingNamespace) - select symbol; - foreach (ISymbol? candidate in candidateStatics) - { - yield return Tuple.Create(false, candidate); - } + public static Tuple SplitOffLastElement(string? qualifiedName) + { + if (qualifiedName is null) + { + return Tuple.Create(null, null); } - internal static T? FirstAncestor(this SyntaxNode startingNode, IReadOnlyCollection doNotPassNodeTypes) - where T : SyntaxNode + int lastPeriod = qualifiedName.LastIndexOf('.'); + if (lastPeriod < 0) { - if (doNotPassNodeTypes is null) - { - throw new ArgumentNullException(nameof(doNotPassNodeTypes)); - } - - SyntaxNode? syntaxNode = startingNode; - while (syntaxNode is object) - { - if (syntaxNode is T result) - { - return result; - } - - if (doNotPassNodeTypes.Any(disallowed => disallowed.GetTypeInfo().IsAssignableFrom(syntaxNode.GetType().GetTypeInfo()))) - { - return default(T); - } - - syntaxNode = syntaxNode.Parent; - } - - return default(T); + return Tuple.Create(null, qualifiedName); } - internal static Tuple SplitOffLastElement(string? qualifiedName) - { - if (qualifiedName is null) - { - return Tuple.Create(null, null); - } + return Tuple.Create(qualifiedName.Substring(0, lastPeriod), qualifiedName.Substring(lastPeriod + 1)); + } - int lastPeriod = qualifiedName.LastIndexOf('.'); - if (lastPeriod < 0) - { - return Tuple.Create(null, qualifiedName); - } + /// + /// Determines whether a given parameter accepts a . + /// + /// The parameter. + /// if the parameter takes a ; otherwise. + public static bool IsCancellationTokenParameter(IParameterSymbol parameterSymbol) => parameterSymbol?.Type.Name == nameof(CancellationToken) && parameterSymbol.Type.BelongsToNamespace(Namespaces.SystemThreading); - return Tuple.Create(qualifiedName.Substring(0, lastPeriod), qualifiedName.Substring(lastPeriod + 1)); - } + public static ISymbol? GetUnderlyingSymbol(IOperation? operation) + { + return operation switch + { + IParameterReferenceOperation paramRef => paramRef.Parameter, + ILocalReferenceOperation localRef => localRef.Local, + IMemberReferenceOperation memberRef => memberRef.Member, + _ => null, + }; + } - /// - /// Determines whether a given parameter accepts a . - /// - /// The parameter. - /// true if the parameter takes a ; false otherwise. - internal static bool IsCancellationTokenParameter(IParameterSymbol parameterSymbol) => parameterSymbol?.Type.Name == nameof(CancellationToken) && parameterSymbol.Type.BelongsToNamespace(Namespaces.SystemThreading); + public static bool IsSameSymbol(IOperation? op1, IOperation? op2) => GetUnderlyingSymbol(op1)?.Equals(GetUnderlyingSymbol(op2), SymbolEqualityComparer.Default) ?? false; - internal static ISymbol? GetUnderlyingSymbol(IOperation? operation) + public static IOperation FindFinalAncestor(IOperation operation) + { + while (operation.Parent is object) { - return operation switch - { - IParameterReferenceOperation paramRef => paramRef.Parameter, - ILocalReferenceOperation localRef => localRef.Local, - IMemberReferenceOperation memberRef => memberRef.Member, - _ => null, - }; + operation = operation.Parent; } - internal static bool IsSameSymbol(IOperation? op1, IOperation? op2) => GetUnderlyingSymbol(op1)?.Equals(GetUnderlyingSymbol(op2)) ?? false; + return operation; + } - internal static IOperation FindFinalAncestor(IOperation operation) + public static T? FindAncestor(IOperation? operation) + where T : class, IOperation + { + while (operation is object) { - while (operation.Parent is object) + if (operation.Parent is T parent) { - operation = operation.Parent; + return parent; } - return operation; + operation = operation.Parent; } - internal static T? FindAncestor(IOperation? operation) - where T : class, IOperation + return default; + } + + public static ISymbol? FindContainingNamedOrAssemblySymbol(this ISymbol? symbol) + { + ISymbol? candidate = symbol; + while (candidate is not null) { - while (operation is object) + if (candidate is INamedTypeSymbol or IAssemblySymbol) { - if (operation.Parent is T parent) - { - return parent; - } - - operation = operation.Parent; + return candidate; } - return default; + candidate = candidate.ContainingSymbol; } - private static bool IsSymbolTheRightType(ISymbol symbol, string typeName, IReadOnlyList namespaces) - { - var fieldSymbol = symbol as IFieldSymbol; - var propertySymbol = symbol as IPropertySymbol; - var parameterSymbol = symbol as IParameterSymbol; - var localSymbol = symbol as ILocalSymbol; - ITypeSymbol? memberType = fieldSymbol?.Type ?? propertySymbol?.Type ?? parameterSymbol?.Type ?? localSymbol?.Type; - return memberType?.Name == typeName && memberType.BelongsToNamespace(namespaces); - } + return null; + } - private static bool IsSymbolTheRightType(ISymbol symbol, string typeName, INamespaceSymbol namespaces) - { - var fieldSymbol = symbol as IFieldSymbol; - var propertySymbol = symbol as IPropertySymbol; - var parameterSymbol = symbol as IParameterSymbol; - var localSymbol = symbol as ILocalSymbol; - ITypeSymbol? memberType = fieldSymbol?.Type ?? propertySymbol?.Type ?? parameterSymbol?.Type ?? localSymbol?.Type; - return memberType?.Name == typeName && memberType.ContainingNamespace.Equals(namespaces); - } + private static bool IsSymbolTheRightType(ISymbol symbol, string typeName, IReadOnlyList namespaces) + { + var fieldSymbol = symbol as IFieldSymbol; + var propertySymbol = symbol as IPropertySymbol; + var parameterSymbol = symbol as IParameterSymbol; + var localSymbol = symbol as ILocalSymbol; + ITypeSymbol? memberType = fieldSymbol?.Type ?? propertySymbol?.Type ?? parameterSymbol?.Type ?? localSymbol?.Type; + return memberType?.Name == typeName && memberType.BelongsToNamespace(namespaces); + } - private static bool LaunchDebuggerExceptionFilter() - { + private static bool IsSymbolTheRightType(ISymbol symbol, string typeName, INamespaceSymbol namespaces) + { + var fieldSymbol = symbol as IFieldSymbol; + var propertySymbol = symbol as IPropertySymbol; + var parameterSymbol = symbol as IParameterSymbol; + var localSymbol = symbol as ILocalSymbol; + ITypeSymbol? memberType = fieldSymbol?.Type ?? propertySymbol?.Type ?? parameterSymbol?.Type ?? localSymbol?.Type; + return memberType?.Name == typeName && memberType.ContainingNamespace.Equals(namespaces, SymbolEqualityComparer.Default); + } + + private static bool LaunchDebuggerExceptionFilter() + { #if DEBUG - System.Diagnostics.Debugger.Launch(); + System.Diagnostics.Debugger.Launch(); #endif - return true; - } + return true; } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD100AsyncVoidMethodAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD100AsyncVoidMethodAnalyzer.cs index 5837dd3f1..023d39afc 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD100AsyncVoidMethodAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD100AsyncVoidMethodAnalyzer.cs @@ -1,71 +1,80 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; - /// - /// Detects the Async Void methods which are NOT used as asynchronous event handlers. - /// - /// - /// [Background] Async void methods have different error-handling semantics. - /// When an exception is thrown out of an async Task or async method/lambda, - /// that exception is captured and placed on the Task object. With async void methods, - /// there is no Task object, so any exceptions thrown out of an async void method will - /// be raised directly on the SynchronizationContext that was active when the async - /// void method started, and it would crash the process. - /// Refer to Stephen's article https://msdn.microsoft.com/en-us/magazine/jj991977.aspx for more info. - /// - /// i.e. - /// - /// - [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] - public class VSTHRD100AsyncVoidMethodAnalyzer : DiagnosticAnalyzer - { - public const string Id = "VSTHRD100"; +namespace Microsoft.VisualStudio.Threading.Analyzers; + +/// +/// Detects the Async Void methods which are NOT used as asynchronous event handlers. +/// +/// +/// [Background] Async void methods have different error-handling semantics. +/// When an exception is thrown out of an async Task or async method/lambda, +/// that exception is captured and placed on the Task object. With async void methods, +/// there is no Task object, so any exceptions thrown out of an async void method will +/// be raised directly on the SynchronizationContext that was active when the async +/// void method started, and it would crash the process. +/// Refer to Stephen's article https://msdn.microsoft.com/en-us/magazine/jj991977.aspx for more info. +/// +/// i.e. +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] +public class VSTHRD100AsyncVoidMethodAnalyzer : DiagnosticAnalyzer +{ + public const string Id = "VSTHRD100"; - internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD100_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD100_MessageFormat), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); + internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD100_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD100_MessageFormat), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); - /// - public override ImmutableArray SupportedDiagnostics + /// + public override ImmutableArray SupportedDiagnostics + { + get { - get - { - return ImmutableArray.Create(Descriptor); - } + return ImmutableArray.Create(Descriptor); } + } - /// - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + /// + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + + context.RegisterSymbolAction(Utils.DebuggableWrapper(AnalyzeNode), SymbolKind.Method); + context.RegisterOperationAction(Utils.DebuggableWrapper(AnalyzeOperation), OperationKind.LocalFunction); + } - context.RegisterSymbolAction(Utils.DebuggableWrapper(this.AnalyzeNode), SymbolKind.Method); + private static void AnalyzeNode(SymbolAnalysisContext context) + { + var methodSymbol = (IMethodSymbol)context.Symbol; + if (methodSymbol.IsAsync && methodSymbol.ReturnsVoid) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptor, methodSymbol.Locations[0])); } + } - private void AnalyzeNode(SymbolAnalysisContext context) + private static void AnalyzeOperation(OperationAnalysisContext context) + { + if (context.Operation is ILocalFunctionOperation localFunctionOperation) { - var methodSymbol = (IMethodSymbol)context.Symbol; + IMethodSymbol methodSymbol = localFunctionOperation.Symbol; if (methodSymbol.IsAsync && methodSymbol.ReturnsVoid) { context.ReportDiagnostic(Diagnostic.Create(Descriptor, methodSymbol.Locations[0])); diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD101AsyncVoidLambdaAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD101AsyncVoidLambdaAnalyzer.cs index e0f217f36..669b10343 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD101AsyncVoidLambdaAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD101AsyncVoidLambdaAnalyzer.cs @@ -1,80 +1,79 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using System.Collections.Immutable; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; - using Microsoft.CodeAnalysis.Operations; +using System.Collections.Immutable; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; - /// - /// Analyzes the async lambdas and checks if they are being used as void-returning delegate types. - /// - /// - /// [Background] Async void methods/lambdas have different error-handling semantics. - /// When an exception is thrown out of an async Task or async method/lambda, - /// that exception is captured and placed on the Task object. With async void methods/lambdas, - /// there is no Task object, so any exceptions thrown out of an async void method/lambda will - /// be raised directly on the SynchronizationContext that was active when the async - /// void method/lambda started, and it would crash the process. - /// Refer to Stephen's article https://msdn.microsoft.com/en-us/magazine/jj991977.aspx for more info. - /// - /// i.e. - /// action) { - /// } - /// - /// void Test() { - /// F(async (x) => { /* This analyzer will report warning on this async lambda. */ - /// DoSomething(); - /// }); - /// } - /// ]]> - /// - [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] - public class VSTHRD101AsyncVoidLambdaAnalyzer : DiagnosticAnalyzer - { - public const string Id = "VSTHRD101"; +namespace Microsoft.VisualStudio.Threading.Analyzers; + +/// +/// Analyzes the async lambdas and checks if they are being used as void-returning delegate types. +/// +/// +/// [Background] Async void methods/lambdas have different error-handling semantics. +/// When an exception is thrown out of an async Task or async method/lambda, +/// that exception is captured and placed on the Task object. With async void methods/lambdas, +/// there is no Task object, so any exceptions thrown out of an async void method/lambda will +/// be raised directly on the SynchronizationContext that was active when the async +/// void method/lambda started, and it would crash the process. +/// Refer to Stephen's article https://msdn.microsoft.com/en-us/magazine/jj991977.aspx for more info. +/// +/// i.e. +/// action) { +/// } +/// +/// void Test() { +/// F(async (x) => { /* This analyzer will report warning on this async lambda. */ +/// DoSomething(); +/// }); +/// } +/// ]]> +/// +[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] +public class VSTHRD101AsyncVoidLambdaAnalyzer : DiagnosticAnalyzer +{ + public const string Id = "VSTHRD101"; - internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD101_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD101_MessageFormat), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); + internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD101_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD101_MessageFormat), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); - /// - public override ImmutableArray SupportedDiagnostics + /// + public override ImmutableArray SupportedDiagnostics + { + get { - get - { - return ImmutableArray.Create(Descriptor); - } + return ImmutableArray.Create(Descriptor); } + } - /// - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + /// + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); - context.RegisterOperationAction( - Utils.DebuggableWrapper(this.AnalyzeOperation), - OperationKind.AnonymousFunction); - } + context.RegisterOperationAction( + Utils.DebuggableWrapper(this.AnalyzeOperation), + OperationKind.AnonymousFunction); + } - private void AnalyzeOperation(OperationAnalysisContext context) + private void AnalyzeOperation(OperationAnalysisContext context) + { + var operation = (IAnonymousFunctionOperation)context.Operation; + IMethodSymbol? methodSymbol = operation.Symbol; + if (methodSymbol is object && methodSymbol.IsAsync && methodSymbol.ReturnsVoid) { - var operation = (IAnonymousFunctionOperation)context.Operation; - IMethodSymbol? methodSymbol = operation.Symbol; - if (methodSymbol is object && methodSymbol.IsAsync && methodSymbol.ReturnsVoid) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptor, operation.Syntax.GetLocation())); - } + context.ReportDiagnostic(Diagnostic.Create(Descriptor, operation.Syntax.GetLocation())); } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD106UseInvokeAsyncForAsyncEventsAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD106UseInvokeAsyncForAsyncEventsAnalyzer.cs index 17a02f4bd..f7aba0c9b 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD106UseInvokeAsyncForAsyncEventsAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD106UseInvokeAsyncForAsyncEventsAnalyzer.cs @@ -1,83 +1,82 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using System.Collections.Immutable; - using System.Diagnostics; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; - using Microsoft.CodeAnalysis.Operations; +using System.Collections.Immutable; +using System.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; - /// - /// Analyzes the usages on AsyncEventHandler delegates and reports warning if - /// they are invoked NOT using the extension method TplExtensions.InvokeAsync() - /// in Microsoft.VisualStudio.Threading assembly. - /// - /// - /// [Background] AsyncEventHandler returns a Task and the default invocation mechanism - /// does not handle the faults thrown from the Tasks. That is why TplExtensions.InvokeAsync() - /// was invented to solve that problem. TplExtensions.InvokeAsync() will ensure all the delegates - /// are executed, aggregate the thrown exceptions, and re-throw the aggregated exception. - /// It is always better to use TplExtensions.InvokeAsync() for AsyncEventHandler delegates. - /// - /// i.e. - /// - /// - [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] - public class VSTHRD106UseInvokeAsyncForAsyncEventsAnalyzer : DiagnosticAnalyzer - { - public const string Id = "VSTHRD106"; +namespace Microsoft.VisualStudio.Threading.Analyzers; - internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD106_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD106_MessageFormat), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); +/// +/// Analyzes the usages on AsyncEventHandler delegates and reports warning if +/// they are invoked NOT using the extension method TplExtensions.InvokeAsync() +/// in Microsoft.VisualStudio.Threading assembly. +/// +/// +/// [Background] AsyncEventHandler returns a Task and the default invocation mechanism +/// does not handle the faults thrown from the Tasks. That is why TplExtensions.InvokeAsync() +/// was invented to solve that problem. TplExtensions.InvokeAsync() will ensure all the delegates +/// are executed, aggregate the thrown exceptions, and re-throw the aggregated exception. +/// It is always better to use TplExtensions.InvokeAsync() for AsyncEventHandler delegates. +/// +/// i.e. +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] +public class VSTHRD106UseInvokeAsyncForAsyncEventsAnalyzer : DiagnosticAnalyzer +{ + public const string Id = "VSTHRD106"; - /// - public override ImmutableArray SupportedDiagnostics - { - get - { - return ImmutableArray.Create(Descriptor); - } - } + internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD106_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD106_MessageFormat), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); - /// - public override void Initialize(AnalysisContext context) + /// + public override ImmutableArray SupportedDiagnostics + { + get { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); - - context.RegisterOperationBlockStartAction(context => - { - // This is a very special case to check if this method is TplExtensions.InvokeAsync(). - // If it is, then do not run the analyzer inside that method. - if (!(context.OwningSymbol.Name == Types.TplExtensions.InvokeAsync && - context.OwningSymbol.ContainingType.Name == Types.TplExtensions.TypeName && - context.OwningSymbol.ContainingType.BelongsToNamespace(Types.TplExtensions.Namespace))) - { - context.RegisterOperationAction(Utils.DebuggableWrapper(this.AnalyzeInvocation), OperationKind.Invocation); - } - }); + return ImmutableArray.Create(Descriptor); } + } + + /// + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); - private void AnalyzeInvocation(OperationAnalysisContext context) + context.RegisterOperationBlockStartAction(context => { - var invocation = (IInvocationOperation)context.Operation; - if (invocation.TargetMethod.ContainingType is { Name: Types.AsyncEventHandler.TypeName } type - && type.BelongsToNamespace(Types.AsyncEventHandler.Namespace)) + // This is a very special case to check if this method is TplExtensions.InvokeAsync(). + // If it is, then do not run the analyzer inside that method. + if (!(context.OwningSymbol.Name == Types.TplExtensions.InvokeAsync && + context.OwningSymbol.ContainingType.Name == Types.TplExtensions.TypeName && + context.OwningSymbol.ContainingType.BelongsToNamespace(Types.TplExtensions.Namespace))) { - context.ReportDiagnostic(Diagnostic.Create(Descriptor, invocation.Syntax.GetLocation())); + context.RegisterOperationAction(Utils.DebuggableWrapper(this.AnalyzeInvocation), OperationKind.Invocation); } + }); + } + + private void AnalyzeInvocation(OperationAnalysisContext context) + { + var invocation = (IInvocationOperation)context.Operation; + if (invocation.TargetMethod.ContainingType is { Name: Types.AsyncEventHandler.TypeName } type + && type.BelongsToNamespace(Types.AsyncEventHandler.Namespace)) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptor, invocation.Syntax.GetLocation())); } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD111UseConfigureAwaitAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD111UseConfigureAwaitAnalyzer.cs index 6bf0bdda9..dfb27c37a 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD111UseConfigureAwaitAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD111UseConfigureAwaitAnalyzer.cs @@ -1,57 +1,56 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System.Collections.Immutable; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +/// +/// Finds await expressions on that do not use . +/// Also works on . +/// +[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] +public class VSTHRD111UseConfigureAwaitAnalyzer : DiagnosticAnalyzer { - using System.Collections.Immutable; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; - using Microsoft.CodeAnalysis.Operations; - - /// - /// Finds await expressions on that do not use . - /// Also works on . - /// - [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] - public class VSTHRD111UseConfigureAwaitAnalyzer : DiagnosticAnalyzer + public const string Id = "VSTHRD111"; + + internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD111_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD111_MessageFormat), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Hidden, // projects should opt IN to this policy + isEnabledByDefault: true); + + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); + + /// + public override void Initialize(AnalysisContext context) { - public const string Id = "VSTHRD111"; - - internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD111_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD111_MessageFormat), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Hidden, // projects should opt IN to this policy - isEnabledByDefault: true); - - /// - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); - - /// - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); - context.RegisterOperationAction(Utils.DebuggableWrapper(this.AnalyzeAwaitOperation), OperationKind.Await); - } + context.RegisterOperationAction(Utils.DebuggableWrapper(this.AnalyzeAwaitOperation), OperationKind.Await); + } - private void AnalyzeAwaitOperation(OperationAnalysisContext context) + private void AnalyzeAwaitOperation(OperationAnalysisContext context) + { + var awaitOperation = (IAwaitOperation)context.Operation; + + // Emit the diagnostic if the awaited expression is a Task or ValueTask. + // They obviously aren't using ConfigureAwait in that case since the awaited expression type would be a + // ConfiguredTaskAwaitable instead. + ITypeSymbol? awaitedTypeInfo = awaitOperation.Operation.Type; + if (awaitedTypeInfo is object && awaitedTypeInfo.BelongsToNamespace(Namespaces.SystemThreadingTasks) && + (awaitedTypeInfo.Name == Types.Task.TypeName || awaitedTypeInfo.Name == Types.ValueTask.TypeName)) { - var awaitOperation = (IAwaitOperation)context.Operation; - - // Emit the diagnostic if the awaited expression is a Task or ValueTask. - // They obviously aren't using ConfigureAwait in that case since the awaited expression type would be a - // ConfiguredTaskAwaitable instead. - ITypeSymbol? awaitedTypeInfo = awaitOperation.Operation.Type; - if (awaitedTypeInfo is object && awaitedTypeInfo.BelongsToNamespace(Namespaces.SystemThreadingTasks) && - (awaitedTypeInfo.Name == Types.Task.TypeName || awaitedTypeInfo.Name == Types.ValueTask.TypeName)) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptor, awaitOperation.Operation.Syntax.GetLocation())); - } + context.ReportDiagnostic(Diagnostic.Create(Descriptor, awaitOperation.Operation.Syntax.GetLocation())); } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD113CheckForSystemIAsyncDisposableAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD113CheckForSystemIAsyncDisposableAnalyzer.cs index 0a468b55e..60c8bbf2d 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD113CheckForSystemIAsyncDisposableAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD113CheckForSystemIAsyncDisposableAnalyzer.cs @@ -1,104 +1,103 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers -{ - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Linq; - using System.Reflection.Emit; - using System.Text; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; - using Microsoft.CodeAnalysis.Operations; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection.Emit; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; - /// - /// Verifies that code that performs type checks for vs-threading's IAsyncDisposable interface also check for System.IAsyncDisposable. - /// - [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] - public class VSTHRD113CheckForSystemIAsyncDisposableAnalyzer : DiagnosticAnalyzer - { - public const string Id = "VSTHRD113"; +namespace Microsoft.VisualStudio.Threading.Analyzers; - internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD113_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD113_MessageFormat), Strings.ResourceManager, typeof(Strings)), - description: new LocalizableResourceString(nameof(Strings.SystemIAsyncDisposablePackageNote), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Info, - isEnabledByDefault: true); +/// +/// Verifies that code that performs type checks for vs-threading's IAsyncDisposable interface also check for System.IAsyncDisposable. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] +public class VSTHRD113CheckForSystemIAsyncDisposableAnalyzer : DiagnosticAnalyzer +{ + public const string Id = "VSTHRD113"; - /// - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); + internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD113_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD113_MessageFormat), Strings.ResourceManager, typeof(Strings)), + description: new LocalizableResourceString(nameof(Strings.SystemIAsyncDisposablePackageNote), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true); - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); - context.RegisterCompilationStartAction(startCompilation => - { - INamedTypeSymbol? vsThreadingAsyncDisposableType = startCompilation.Compilation.GetTypeByMetadataName(Types.IAsyncDisposable.FullName); - INamedTypeSymbol? bclAsyncDisposableType = startCompilation.Compilation.GetTypeByMetadataName(Types.BclAsyncDisposable.FullName); - if (vsThreadingAsyncDisposableType is object) - { - startCompilation.RegisterOperationAction(Utils.DebuggableWrapper(c => AnalyzeTypeCheck(c, vsThreadingAsyncDisposableType, bclAsyncDisposableType)), OperationKind.IsType); - startCompilation.RegisterOperationAction(Utils.DebuggableWrapper(c => AnalyzeTypeCheck(c, vsThreadingAsyncDisposableType, bclAsyncDisposableType)), OperationKind.IsPattern); - startCompilation.RegisterOperationAction(Utils.DebuggableWrapper(c => AnalyzeTypeCheck(c, vsThreadingAsyncDisposableType, bclAsyncDisposableType)), OperationKind.Conversion); - } - }); - } + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); - private static void AnalyzeTypeCheck(OperationAnalysisContext context, INamedTypeSymbol vsThreadingAsyncDisposableType, INamedTypeSymbol bclAsyncDisposableType) + context.RegisterCompilationStartAction(startCompilation => { - switch (context.Operation) + INamedTypeSymbol? vsThreadingAsyncDisposableType = startCompilation.Compilation.GetTypeByMetadataName(Types.IAsyncDisposable.FullName); + INamedTypeSymbol? bclAsyncDisposableType = startCompilation.Compilation.GetTypeByMetadataName(Types.BclAsyncDisposable.FullName); + if (vsThreadingAsyncDisposableType is object) { - case IIsTypeOperation { TypeOperand: { } operand }: - ConsiderTypeCheck(context, operand, vsThreadingAsyncDisposableType, bclAsyncDisposableType); - break; - case IIsPatternOperation { Pattern: IDeclarationPatternOperation { DeclaredSymbol: ILocalSymbol { Type: { } operand } } }: - ConsiderTypeCheck(context, operand, vsThreadingAsyncDisposableType, bclAsyncDisposableType); - break; - case IConversionOperation { Type: { } operand }: - ConsiderTypeCheck(context, operand, vsThreadingAsyncDisposableType, bclAsyncDisposableType); - break; + startCompilation.RegisterOperationAction(Utils.DebuggableWrapper(c => AnalyzeTypeCheck(c, vsThreadingAsyncDisposableType, bclAsyncDisposableType)), OperationKind.IsType); + startCompilation.RegisterOperationAction(Utils.DebuggableWrapper(c => AnalyzeTypeCheck(c, vsThreadingAsyncDisposableType, bclAsyncDisposableType)), OperationKind.IsPattern); + startCompilation.RegisterOperationAction(Utils.DebuggableWrapper(c => AnalyzeTypeCheck(c, vsThreadingAsyncDisposableType, bclAsyncDisposableType)), OperationKind.Conversion); } + }); + } - static void ConsiderTypeCheck(OperationAnalysisContext context, ITypeSymbol operand, INamedTypeSymbol vsThreadingAsyncDisposableType, INamedTypeSymbol bclAsyncDisposableType) + private static void AnalyzeTypeCheck(OperationAnalysisContext context, INamedTypeSymbol vsThreadingAsyncDisposableType, INamedTypeSymbol? bclAsyncDisposableType) + { + switch (context.Operation) + { + case IIsTypeOperation { TypeOperand: { } operand }: + ConsiderTypeCheck(context, operand, vsThreadingAsyncDisposableType, bclAsyncDisposableType); + break; + case IIsPatternOperation { Pattern: IDeclarationPatternOperation { DeclaredSymbol: ILocalSymbol { Type: { } operand } } }: + ConsiderTypeCheck(context, operand, vsThreadingAsyncDisposableType, bclAsyncDisposableType); + break; + case IConversionOperation { Type: { } operand }: + ConsiderTypeCheck(context, operand, vsThreadingAsyncDisposableType, bclAsyncDisposableType); + break; + } + + static void ConsiderTypeCheck(OperationAnalysisContext context, ITypeSymbol operand, INamedTypeSymbol vsThreadingAsyncDisposableType, INamedTypeSymbol? bclAsyncDisposableType) + { + if (SymbolEqualityComparer.Default.Equals(vsThreadingAsyncDisposableType, operand)) { - if (Equals(vsThreadingAsyncDisposableType, operand)) + // If the System.IAsyncDisposable type is defined, search for a check for that type and skip the diagnostic if we find one. + if (bclAsyncDisposableType is object) { - // If the System.IAsyncDisposable type is defined, search for a check for that type and skip the diagnostic if we find one. - if (bclAsyncDisposableType is object) + IOperation methodBlock = Utils.FindFinalAncestor(context.Operation); + if (methodBlock.Descendants().Any(op => IsTypeCheck(op, bclAsyncDisposableType))) { - IOperation methodBlock = Utils.FindFinalAncestor(context.Operation); - if (methodBlock.Descendants().Any(op => IsTypeCheck(op, bclAsyncDisposableType))) - { - // We found a matching check for the BCL type. No diagnostic to report. - return; - } + // We found a matching check for the BCL type. No diagnostic to report. + return; } - - context.ReportDiagnostic(Diagnostic.Create(Descriptor, context.Operation.Syntax.GetLocation())); } + + context.ReportDiagnostic(Diagnostic.Create(Descriptor, context.Operation.Syntax.GetLocation())); } + } - static bool IsTypeCheck(IOperation operation, INamedTypeSymbol typeChecked) + static bool IsTypeCheck(IOperation operation, INamedTypeSymbol typeChecked) + { + switch (operation) { - switch (operation) - { - case IIsTypeOperation { TypeOperand: { } operand }: - return Equals(typeChecked, operand); - case IDeclarationPatternOperation { DeclaredSymbol: ILocalSymbol { Type: { } operand } }: - return Equals(typeChecked, operand); - case IConversionOperation { Type: { } operand }: - return Equals(typeChecked, operand); - default: - return false; - } + case IIsTypeOperation { TypeOperand: { } operand }: + return SymbolEqualityComparer.Default.Equals(typeChecked, operand); + case IDeclarationPatternOperation { DeclaredSymbol: ILocalSymbol { Type: { } operand } }: + return SymbolEqualityComparer.Default.Equals(typeChecked, operand); + case IConversionOperation { Type: { } operand }: + return SymbolEqualityComparer.Default.Equals(typeChecked, operand); + default: + return false; } } } diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD115AvoidJoinableTaskContextCtorWithNullArgsAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD115AvoidJoinableTaskContextCtorWithNullArgsAnalyzer.cs new file mode 100644 index 000000000..a358185c0 --- /dev/null +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD115AvoidJoinableTaskContextCtorWithNullArgsAnalyzer.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +/// +/// Flags the use of new JoinableTaskContext(null, null) and advises using JoinableTaskContext.CreateNoOpContext instead. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] +public class VSTHRD115AvoidJoinableTaskContextCtorWithNullArgsAnalyzer : DiagnosticAnalyzer +{ + public const string Id = "VSTHRD115"; + + public const string UsesDefaultThreadPropertyName = "UsesDefaultThread"; + + public const string NodeTypePropertyName = "NodeType"; + + public const string NodeTypeArgument = "Argument"; + + public const string NodeTypeCreation = "Creation"; + + internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD115_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD115_MessageFormat), Strings.ResourceManager, typeof(Strings)), + description: null, + helpLinkUri: Utils.GetHelpLink(Id), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + + context.RegisterCompilationStartAction(startCompilation => + { + INamedTypeSymbol? joinableTaskContextType = startCompilation.Compilation.GetTypeByMetadataName(Types.JoinableTaskContext.FullName); + if (joinableTaskContextType is not null) + { + IMethodSymbol? problematicCtor = joinableTaskContextType.InstanceConstructors.SingleOrDefault(ctor => ctor.Parameters.Length == 2 && ctor.Parameters[0].Type.Name == nameof(Thread) && ctor.Parameters[1].Type.Name == nameof(SynchronizationContext)); + + if (problematicCtor is not null && joinableTaskContextType.GetMembers(Types.JoinableTaskContext.CreateNoOpContext).Length > 0) + { + startCompilation.RegisterOperationAction(Utils.DebuggableWrapper(c => this.AnalyzeObjectCreation(c, joinableTaskContextType, problematicCtor)), OperationKind.ObjectCreation); + } + } + }); + } + + private void AnalyzeObjectCreation(OperationAnalysisContext context, INamedTypeSymbol joinableTaskContextType, IMethodSymbol problematicCtor) + { + IObjectCreationOperation creation = (IObjectCreationOperation)context.Operation; + if (SymbolEqualityComparer.Default.Equals(creation.Constructor, problematicCtor)) + { + // Only flag if "null" is passed in as the constructor's second argument (explicitly or implicitly). + if (creation.Arguments.Length == 2) + { + IOperation arg2 = creation.Arguments[1].Value; + if (arg2 is IConversionOperation { Operand: ILiteralOperation { ConstantValue: { HasValue: true, Value: null } } literal }) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptor, literal.Syntax.GetLocation(), CreateProperties(NodeTypeArgument))); + } + else if (arg2 is IDefaultValueOperation { ConstantValue: { HasValue: true, Value: null } }) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptor, creation.Syntax.GetLocation(), CreateProperties(NodeTypeCreation))); + } + + ImmutableDictionary CreateProperties(string nodeType) + { + // The caller is using the default thread if they omit the argument, pass in "null", or pass in "Thread.CurrentThread". + // At the moment, we are not testing for the Thread.CurrentThread case. + bool usesDefaultThread = creation.Arguments[0].Value is IDefaultValueOperation { ConstantValue: { HasValue: true, Value: null } } + or IConversionOperation { Operand: ILiteralOperation { ConstantValue: { HasValue: true, Value: null } } }; + + return ImmutableDictionary.Create() + .Add(UsesDefaultThreadPropertyName, usesDefaultThread ? "true" : "false") + .Add(NodeTypePropertyName, nodeType); + } + } + } + } +} diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD200UseAsyncNamingConventionAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD200UseAsyncNamingConventionAnalyzer.cs index 795371249..4a335fd75 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD200UseAsyncNamingConventionAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/VSTHRD200UseAsyncNamingConventionAnalyzer.cs @@ -1,108 +1,134 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.VisualStudio.Threading.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] +public class VSTHRD200UseAsyncNamingConventionAnalyzer : DiagnosticAnalyzer { - using System; - using System.Collections.Immutable; - using System.Linq; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; - - [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] - public class VSTHRD200UseAsyncNamingConventionAnalyzer : DiagnosticAnalyzer + public const string Id = "VSTHRD200"; + + public const string NewNameKey = "NewName"; + + public const string MandatoryAsyncSuffix = "Async"; + + public static readonly DiagnosticDescriptor AddAsyncDescriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD200_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD200_AddAsync_MessageFormat), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Style", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor RemoveAsyncDescriptor = new DiagnosticDescriptor( + id: Id, + title: new LocalizableResourceString(nameof(Strings.VSTHRD200_Title), Strings.ResourceManager, typeof(Strings)), + messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD200_RemoveAsync_MessageFormat), Strings.ResourceManager, typeof(Strings)), + helpLinkUri: Utils.GetHelpLink(Id), + category: "Style", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + AddAsyncDescriptor, + RemoveAsyncDescriptor); + + /// + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + + context.RegisterCompilationStartAction(context => + { + CommonInterest.AwaitableTypeTester awaitableTypes = CommonInterest.CollectAwaitableTypes(context.Compilation, context.CancellationToken); + context.RegisterSymbolAction(Utils.DebuggableWrapper(context => this.AnalyzeNode(context, awaitableTypes)), SymbolKind.Method); + context.RegisterOperationAction(Utils.DebuggableWrapper(context => this.AnalyzeLocalFunction(context, awaitableTypes)), OperationKind.LocalFunction); + }); + } + + private void AnalyzeLocalFunction(OperationAnalysisContext context, CommonInterest.AwaitableTypeTester awaitableTypes) + { + if (this.AnalyzeMethodSymbol(context.Compilation, ((ILocalFunctionOperation)context.Operation).Symbol, awaitableTypes, context.CancellationToken) is Diagnostic diagnostic) + { + context.ReportDiagnostic(diagnostic); + } + } + + private void AnalyzeNode(SymbolAnalysisContext context, CommonInterest.AwaitableTypeTester awaitableTypes) { - public const string Id = "VSTHRD200"; - - internal const string NewNameKey = "NewName"; - - internal const string MandatoryAsyncSuffix = "Async"; - - internal static readonly DiagnosticDescriptor AddAsyncDescriptor = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD200_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD200_AddAsync_MessageFormat), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Style", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); - - internal static readonly DiagnosticDescriptor RemoveAsyncDescriptor = new DiagnosticDescriptor( - id: Id, - title: new LocalizableResourceString(nameof(Strings.VSTHRD200_Title), Strings.ResourceManager, typeof(Strings)), - messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD200_RemoveAsync_MessageFormat), Strings.ResourceManager, typeof(Strings)), - helpLinkUri: Utils.GetHelpLink(Id), - category: "Style", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); - - /// - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( - AddAsyncDescriptor, - RemoveAsyncDescriptor); - - /// - public override void Initialize(AnalysisContext context) + if (this.AnalyzeMethodSymbol(context.Compilation, (IMethodSymbol)context.Symbol, awaitableTypes, context.CancellationToken) is Diagnostic diagnostic) { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + context.ReportDiagnostic(diagnostic); + } + } + + private Diagnostic? AnalyzeMethodSymbol(Compilation compilation, IMethodSymbol methodSymbol, CommonInterest.AwaitableTypeTester awaitableTypes, CancellationToken cancellationToken) + { + if (methodSymbol.AssociatedSymbol is IPropertySymbol) + { + // Skip accessor methods associated with properties. + return null; + } + + // Skip entrypoint methods since their name is non-negotiable. + if (Utils.IsEntrypointMethod(methodSymbol, compilation, cancellationToken)) + { + return null; + } - context.RegisterSymbolAction(Utils.DebuggableWrapper(new PerCompilation().AnalyzeNode), SymbolKind.Method); + // Skip the method of the recommended dispose pattern. + if (methodSymbol.Name == "DisposeAsyncCore") + { + return null; } - private class PerCompilation : DiagnosticAnalyzerState + bool hasAsyncFocusedReturnType = Utils.HasAsyncCompatibleReturnType(methodSymbol); + + bool actuallyEndsWithAsync = methodSymbol.Name.EndsWith(MandatoryAsyncSuffix, StringComparison.CurrentCulture); + + if (hasAsyncFocusedReturnType != actuallyEndsWithAsync) { - internal void AnalyzeNode(SymbolAnalysisContext context) + // Now that we have done the cheap checks to find that this method may deserve a diagnostic, + // Do deeper checks to skip over methods that implement API contracts that are controlled elsewhere. + if (methodSymbol.FindInterfacesImplemented().Any() || methodSymbol.IsOverride) { - var methodSymbol = (IMethodSymbol)context.Symbol; - if (methodSymbol.AssociatedSymbol is IPropertySymbol) - { - // Skip accessor methods associated with properties. - return; - } - - // Skip entrypoint methods since their name is non-negotiable. - if (Utils.IsEntrypointMethod(methodSymbol, context.Compilation, context.CancellationToken)) - { - return; - } - - bool hasAsyncFocusedReturnType = Utils.HasAsyncCompatibleReturnType(methodSymbol); - - bool actuallyEndsWithAsync = methodSymbol.Name.EndsWith(MandatoryAsyncSuffix, StringComparison.CurrentCulture); - - if (hasAsyncFocusedReturnType != actuallyEndsWithAsync) - { - // Now that we have done the cheap checks to find that this method may deserve a diagnostic, - // Do deeper checks to skip over methods that implement API contracts that are controlled elsewhere. - if (methodSymbol.FindInterfacesImplemented().Any() || methodSymbol.IsOverride) - { - return; - } - - if (hasAsyncFocusedReturnType) - { - // We actively encourage folks to use the Async keyword only for clearly async-focused types. - // Not just any awaitable, since some stray extension method shouldn't change the world for everyone. - ImmutableDictionary? properties = ImmutableDictionary.Empty - .Add(NewNameKey, methodSymbol.Name + MandatoryAsyncSuffix); - context.ReportDiagnostic(Diagnostic.Create( - AddAsyncDescriptor, - methodSymbol.Locations[0], - properties)); - } - else if (!this.IsAwaitableType(methodSymbol.ReturnType, context.Compilation, context.CancellationToken)) - { - // Only warn about abusing the Async suffix if the return type is not awaitable. - ImmutableDictionary? properties = ImmutableDictionary.Empty - .Add(NewNameKey, methodSymbol.Name.Substring(0, methodSymbol.Name.Length - MandatoryAsyncSuffix.Length)); - context.ReportDiagnostic(Diagnostic.Create( - RemoveAsyncDescriptor, - methodSymbol.Locations[0], - properties)); - } - } + return null; + } + + if (hasAsyncFocusedReturnType) + { + // We actively encourage folks to use the Async keyword only for clearly async-focused types. + // Not just any awaitable, since some stray extension method shouldn't change the world for everyone. + ImmutableDictionary? properties = ImmutableDictionary.Empty + .Add(NewNameKey, methodSymbol.Name + MandatoryAsyncSuffix); + return Diagnostic.Create( + AddAsyncDescriptor, + methodSymbol.Locations[0], + properties); + } + else if (!awaitableTypes.IsAwaitableType(methodSymbol.ReturnType)) + { + // Only warn about abusing the Async suffix if the return type is not awaitable. + ImmutableDictionary? properties = ImmutableDictionary.Empty + .Add(NewNameKey, methodSymbol.Name.Substring(0, methodSymbol.Name.Length - MandatoryAsyncSuffix.Length)); + return Diagnostic.Create( + RemoveAsyncDescriptor, + methodSymbol.Locations[0], + properties); } } + + return null; } } diff --git a/src/Microsoft.VisualStudio.Threading.JointPackage/Microsoft.VisualStudio.Threading.JointPackage.csproj b/src/Microsoft.VisualStudio.Threading.JointPackage/Microsoft.VisualStudio.Threading.JointPackage.csproj new file mode 100644 index 000000000..6d30fbae5 --- /dev/null +++ b/src/Microsoft.VisualStudio.Threading.JointPackage/Microsoft.VisualStudio.Threading.JointPackage.csproj @@ -0,0 +1,20 @@ + + + + Microsoft.VisualStudio.Threading + false + false + $(NoWarn);NU5128;MSB3277 + false + + + + + + + + + + + + diff --git a/src/Microsoft.VisualStudio.Threading/AsyncAutoResetEvent.cs b/src/Microsoft.VisualStudio.Threading/AsyncAutoResetEvent.cs index 3253611f3..aa0d5d6a8 100644 --- a/src/Microsoft.VisualStudio.Threading/AsyncAutoResetEvent.cs +++ b/src/Microsoft.VisualStudio.Threading/AsyncAutoResetEvent.cs @@ -1,191 +1,188 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// An asynchronous implementation of an AutoResetEvent. +/// +[DebuggerDisplay("Signaled: {signaled}")] +public class AsyncAutoResetEvent { - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Linq; - using System.Text; - using System.Threading; - using System.Threading.Tasks; + /// + /// A queue of folks awaiting signals. + /// + private readonly Queue signalAwaiters = new Queue(); /// - /// An asynchronous implementation of an AutoResetEvent. + /// Whether to complete the task synchronously in the method, + /// as opposed to asynchronously. /// - [DebuggerDisplay("Signaled: {signaled}")] - public class AsyncAutoResetEvent - { - /// - /// A queue of folks awaiting signals. - /// - private readonly Queue signalAwaiters = new Queue(); + private readonly bool allowInliningAwaiters; - /// - /// Whether to complete the task synchronously in the method, - /// as opposed to asynchronously. - /// - private readonly bool allowInliningAwaiters; + /// + /// A reusable delegate that points to the method. + /// + private readonly Action onCancellationRequestHandler; - /// - /// A reusable delegate that points to the method. - /// - private readonly Action onCancellationRequestHandler; + /// + /// A value indicating whether this event is already in a signaled state. + /// + /// + /// This should not need the volatile modifier because it is + /// always accessed within a lock. + /// + private bool signaled; - /// - /// A value indicating whether this event is already in a signaled state. - /// - /// - /// This should not need the volatile modifier because it is - /// always accessed within a lock. - /// - private bool signaled; + /// + /// Initializes a new instance of the class + /// that does not inline awaiters. + /// + public AsyncAutoResetEvent() + : this(allowInliningAwaiters: false) + { + } - /// - /// Initializes a new instance of the class - /// that does not inline awaiters. - /// - public AsyncAutoResetEvent() - : this(allowInliningAwaiters: false) - { - } + /// + /// Initializes a new instance of the class. + /// + /// + /// A value indicating whether to complete the task synchronously in the method, + /// as opposed to asynchronously. better simulates the behavior of the + /// class, but can result in slightly better performance. + /// + public AsyncAutoResetEvent(bool allowInliningAwaiters) + { + this.allowInliningAwaiters = allowInliningAwaiters; + this.onCancellationRequestHandler = this.OnCancellationRequest; + } - /// - /// Initializes a new instance of the class. - /// - /// - /// A value indicating whether to complete the task synchronously in the method, - /// as opposed to asynchronously. false better simulates the behavior of the - /// class, but true can result in slightly better performance. - /// - public AsyncAutoResetEvent(bool allowInliningAwaiters) - { - this.allowInliningAwaiters = allowInliningAwaiters; - this.onCancellationRequestHandler = this.OnCancellationRequest; - } + /// + /// Returns an awaitable that may be used to asynchronously acquire the next signal. + /// + /// An awaitable. + public Task WaitAsync() + { + return this.WaitAsync(CancellationToken.None); + } - /// - /// Returns an awaitable that may be used to asynchronously acquire the next signal. - /// - /// An awaitable. - public Task WaitAsync() + /// + /// Returns an awaitable that may be used to asynchronously acquire the next signal. + /// + /// A token whose cancellation removes the caller from the queue of those waiting for the event. + /// An awaitable. + public Task WaitAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) { - return this.WaitAsync(CancellationToken.None); + return Task.FromCanceled(cancellationToken); } - /// - /// Returns an awaitable that may be used to asynchronously acquire the next signal. - /// - /// A token whose cancellation removes the caller from the queue of those waiting for the event. - /// An awaitable. - public Task WaitAsync(CancellationToken cancellationToken) + lock (this.signalAwaiters) { - if (cancellationToken.IsCancellationRequested) + if (this.signaled) { - return Task.FromCanceled(cancellationToken); + this.signaled = false; + return Task.CompletedTask; } - - lock (this.signalAwaiters) + else { - if (this.signaled) + var waiter = new WaiterCompletionSource(this, this.allowInliningAwaiters, cancellationToken); + if (cancellationToken.IsCancellationRequested) { - this.signaled = false; - return Task.CompletedTask; + waiter.TrySetCanceled(cancellationToken); } else { - var waiter = new WaiterCompletionSource(this, this.allowInliningAwaiters, cancellationToken); - if (cancellationToken.IsCancellationRequested) - { - waiter.TrySetCanceled(cancellationToken); - } - else - { - this.signalAwaiters.Enqueue(waiter); - } - - return waiter.Task; + this.signalAwaiters.Enqueue(waiter); } + + return waiter.Task; } } + } - /// - /// Unblocks one waiter or sets the signal if no waiters are present so the next waiter may proceed immediately. - /// - public void Set() + /// + /// Unblocks one waiter or sets the signal if no waiters are present so the next waiter may proceed immediately. + /// + public void Set() + { + WaiterCompletionSource? toRelease = null; + lock (this.signalAwaiters) { - WaiterCompletionSource? toRelease = null; - lock (this.signalAwaiters) + if (this.signalAwaiters.Count > 0) { - if (this.signalAwaiters.Count > 0) - { - toRelease = this.signalAwaiters.Dequeue(); - } - else if (!this.signaled) - { - this.signaled = true; - } + toRelease = this.signalAwaiters.Dequeue(); } - - if (toRelease is object) + else if (!this.signaled) { - toRelease.Registration.Dispose(); - toRelease.TrySetResult(default(EmptyStruct)); + this.signaled = true; } } - /// - /// Responds to cancellation requests by removing the request from the waiter queue. - /// - /// The passed in to the method. - private void OnCancellationRequest(object state) + if (toRelease is object) { - var tcs = (WaiterCompletionSource)state; - bool removed; - lock (this.signalAwaiters) - { - removed = this.signalAwaiters.RemoveMidQueue(tcs); - } + toRelease.Registration.Dispose(); + toRelease.TrySetResult(default(EmptyStruct)); + } + } - // We only cancel the task if we removed it from the queue. - // If it wasn't in the queue, either it has already been signaled - // or it hasn't even been added to the queue yet. If the latter, - // the Task will be canceled later so long as the signal hasn't been awarded - // to this Task yet. - if (removed) - { - tcs.TrySetCanceled(tcs.CancellationToken); - } + /// + /// Responds to cancellation requests by removing the request from the waiter queue. + /// + /// The passed in to the method. + private void OnCancellationRequest(object state) + { + var tcs = (WaiterCompletionSource)state; + bool removed; + lock (this.signalAwaiters) + { + removed = this.signalAwaiters.RemoveMidQueue(tcs); } + // We only cancel the task if we removed it from the queue. + // If it wasn't in the queue, either it has already been signaled + // or it hasn't even been added to the queue yet. If the latter, + // the Task will be canceled later so long as the signal hasn't been awarded + // to this Task yet. + if (removed) + { + tcs.TrySetCanceled(tcs.CancellationToken); + } + } + + /// + /// Tracks someone waiting for a signal from the event. + /// + private class WaiterCompletionSource : TaskCompletionSource + { /// - /// Tracks someone waiting for a signal from the event. + /// Initializes a new instance of the class. /// - private class WaiterCompletionSource : TaskCompletionSourceWithoutInlining + /// The event that is initializing this value. + /// to allow continuations to be inlined upon the completer's callstack. + /// The cancellation token associated with the waiter. + internal WaiterCompletionSource(AsyncAutoResetEvent owner, bool allowInliningContinuations, CancellationToken cancellationToken) + : base(allowInliningContinuations ? TaskCreationOptions.None : TaskCreationOptions.RunContinuationsAsynchronously) { - /// - /// Initializes a new instance of the class. - /// - /// The event that is initializing this value. - /// true to allow continuations to be inlined upon the completer's callstack. - /// The cancellation token associated with the waiter. - internal WaiterCompletionSource(AsyncAutoResetEvent owner, bool allowInliningContinuations, CancellationToken cancellationToken) - : base(allowInliningContinuations) - { - this.CancellationToken = cancellationToken; - this.Registration = cancellationToken.Register(NullableHelpers.AsNullableArgAction(owner.onCancellationRequestHandler), this); - } + this.CancellationToken = cancellationToken; + this.Registration = cancellationToken.Register(NullableHelpers.AsNullableArgAction(owner.onCancellationRequestHandler), this); + } - /// - /// Gets the provided by the waiter. - /// - internal CancellationToken CancellationToken { get; private set; } + /// + /// Gets the provided by the waiter. + /// + internal CancellationToken CancellationToken { get; private set; } - /// - /// Gets the registration to dispose of when the waiter receives their event. - /// - internal CancellationTokenRegistration Registration { get; private set; } - } + /// + /// Gets the registration to dispose of when the waiter receives their event. + /// + internal CancellationTokenRegistration Registration { get; private set; } } } diff --git a/src/Microsoft.VisualStudio.Threading/AsyncBarrier.cs b/src/Microsoft.VisualStudio.Threading/AsyncBarrier.cs index e1d6f5161..92d273663 100644 --- a/src/Microsoft.VisualStudio.Threading/AsyncBarrier.cs +++ b/src/Microsoft.VisualStudio.Threading/AsyncBarrier.cs @@ -1,79 +1,112 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// An asynchronous barrier that blocks the signaler until all other participants have signaled. +/// +public class AsyncBarrier { - using System; - using System.Collections.Concurrent; - using System.Collections.Generic; - using System.Linq; - using System.Text; - using System.Threading; - using System.Threading.Tasks; + /// + /// The number of participants being synchronized. + /// + private readonly int participantCount; /// - /// An asynchronous barrier that blocks the signaler until all other participants have signaled. + /// The set of participants who have reached the barrier, with their awaiters that can resume those participants. /// - public class AsyncBarrier - { - /// - /// The number of participants being synchronized. - /// - private readonly int participantCount; + private readonly Stack waiters; - /// - /// The set of participants who have reached the barrier, with their awaiters that can resume those participants. - /// - private readonly Stack> waiters; + /// + /// Initializes a new instance of the class. + /// + /// The number of participants. + public AsyncBarrier(int participants) + { + Requires.Range(participants > 0, nameof(participants)); + this.participantCount = participants; - /// - /// Initializes a new instance of the class. - /// - /// The number of participants. - public AsyncBarrier(int participants) - { - Requires.Range(participants > 0, nameof(participants)); - this.participantCount = participants; + // Allocate the stack so no resizing is necessary. + // We don't need space for the last participant, since we never have to store it. + this.waiters = new Stack(participants - 1); + } - // Allocate the stack so no resizing is necessary. - // We don't need space for the last participant, since we never have to store it. - this.waiters = new Stack>(participants - 1); - } + /// + public Task SignalAndWait() => this.SignalAndWait(CancellationToken.None).AsTask(); - /// - /// Signals that a participant is ready, and returns a Task - /// that completes when all other participants have also signaled ready. - /// - /// A Task, which will complete (or may already be completed) when the last participant calls this method. - public Task SignalAndWait() + /// + /// Signals that a participant is ready, and returns a Task + /// that completes when all other participants have also signaled ready. + /// + /// + /// A token that signals the caller's lost interest in waiting. + /// The signal effect of the method is not canceled with the token. + /// + /// A task which will complete (or may already be completed) when the last participant calls this method. + public ValueTask SignalAndWait(CancellationToken cancellationToken) + { + lock (this.waiters) { - lock (this.waiters) + if (this.waiters.Count + 1 == this.participantCount) { - if (this.waiters.Count + 1 == this.participantCount) + // This is the last one we were waiting for. + // Unleash everyone that preceded this one. + while (this.waiters.Count > 0) { - // This is the last one we were waiting for. - // Unleash everyone that preceded this one. - while (this.waiters.Count > 0) - { - Task.Factory.StartNew( - state => ((TaskCompletionSource)state!).SetResult(default(EmptyStruct)), - this.waiters.Pop(), - CancellationToken.None, - TaskCreationOptions.None, - TaskScheduler.Default); - } + Waiter waiter = this.waiters.Pop(); + waiter.CompletionSource.TrySetResult(default); + waiter.CancellationRegistration.Dispose(); + } - // And allow this one to continue immediately. - return Task.CompletedTask; + // And allow this one to continue immediately. + return new ValueTask(cancellationToken.IsCancellationRequested + ? Task.FromCanceled(cancellationToken) + : Task.CompletedTask); + } + else + { + // We need more folks. So suspend this caller. + TaskCompletionSource tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + CancellationTokenRegistration ctr; + if (cancellationToken.CanBeCanceled) + { +#if NET + ctr = cancellationToken.Register( + static (tcs, ct) => ((TaskCompletionSource)tcs!).TrySetCanceled(ct), tcs); +#else + ctr = cancellationToken.Register( + static s => + { + var t = (Tuple, CancellationToken>)s!; + t.Item1.TrySetCanceled(t.Item2); + }, + Tuple.Create(tcs, cancellationToken)); +#endif } else { - // We need more folks. So suspend this caller. - var tcs = new TaskCompletionSource(); - this.waiters.Push(tcs); - return tcs.Task; + ctr = default; } + + this.waiters.Push(new Waiter(tcs, ctr)); + return new ValueTask(tcs.Task); } } } + + private readonly struct Waiter(TaskCompletionSource completionSource, CancellationTokenRegistration cancellationRegistration) + { + internal readonly TaskCompletionSource CompletionSource => completionSource; + + internal readonly CancellationTokenRegistration CancellationRegistration => cancellationRegistration; + } } diff --git a/src/Microsoft.VisualStudio.Threading/AsyncCountdownEvent.cs b/src/Microsoft.VisualStudio.Threading/AsyncCountdownEvent.cs index 5d766fc30..fa9055659 100644 --- a/src/Microsoft.VisualStudio.Threading/AsyncCountdownEvent.cs +++ b/src/Microsoft.VisualStudio.Threading/AsyncCountdownEvent.cs @@ -1,114 +1,113 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// An asynchronous style countdown event. +/// +public class AsyncCountdownEvent { - using System; - using System.Collections.Generic; - using System.ComponentModel; - using System.Linq; - using System.Text; - using System.Threading; - using System.Threading.Tasks; + /// + /// The manual reset event we use to signal all awaiters. + /// + private readonly AsyncManualResetEvent manualEvent; + + /// + /// The remaining number of signals required before we can unblock waiters. + /// + private int remainingCount; /// - /// An asynchronous style countdown event. + /// Initializes a new instance of the class. /// - public class AsyncCountdownEvent + /// The number of signals required to unblock awaiters. + public AsyncCountdownEvent(int initialCount) { - /// - /// The manual reset event we use to signal all awaiters. - /// - private readonly AsyncManualResetEvent manualEvent; + Requires.Range(initialCount >= 0, "initialCount"); + this.manualEvent = new AsyncManualResetEvent(initialCount == 0); + this.remainingCount = initialCount; + } - /// - /// The remaining number of signals required before we can unblock waiters. - /// - private int remainingCount; + /// + /// Returns an awaitable that executes the continuation when the countdown reaches zero. + /// + /// An awaitable. + public Task WaitAsync() + { + return this.manualEvent.WaitAsync(); + } - /// - /// Initializes a new instance of the class. - /// - /// The number of signals required to unblock awaiters. - public AsyncCountdownEvent(int initialCount) + /// + /// Decrements the counter by one. + /// + /// + /// A task that completes when the signal has been set if this call causes the count to reach zero. + /// If the count is not zero, a completed task is returned. + /// + /// + /// + /// On .NET versions prior to 4.6: + /// This method may return before the signal set has propagated. + /// The returned task completes when the signal has definitely been set. + /// + /// + /// On .NET 4.6 and later: + /// This method is not asynchronous. The returned Task is always completed. + /// + /// + [Obsolete("Use Signal() instead."), EditorBrowsable(EditorBrowsableState.Never)] + public async Task SignalAsync() + { + int newCount = Interlocked.Decrement(ref this.remainingCount); + if (newCount == 0) { - Requires.Range(initialCount >= 0, "initialCount"); - this.manualEvent = new AsyncManualResetEvent(initialCount == 0); - this.remainingCount = initialCount; + await this.manualEvent.SetAsync().ConfigureAwait(false); } - - /// - /// Returns an awaitable that executes the continuation when the countdown reaches zero. - /// - /// An awaitable. - public Task WaitAsync() + else if (newCount < 0) { - return this.manualEvent.WaitAsync(); + throw new InvalidOperationException(); } + } - /// - /// Decrements the counter by one. - /// - /// - /// A task that completes when the signal has been set if this call causes the count to reach zero. - /// If the count is not zero, a completed task is returned. - /// - /// - /// - /// On .NET versions prior to 4.6: - /// This method may return before the signal set has propagated. - /// The returned task completes when the signal has definitely been set. - /// - /// - /// On .NET 4.6 and later: - /// This method is not asynchronous. The returned Task is always completed. - /// - /// - [Obsolete("Use Signal() instead."), EditorBrowsable(EditorBrowsableState.Never)] - public async Task SignalAsync() + /// + /// Decrements the counter by one. + /// + public void Signal() + { + int newCount = Interlocked.Decrement(ref this.remainingCount); + if (newCount == 0) { - int newCount = Interlocked.Decrement(ref this.remainingCount); - if (newCount == 0) - { - await this.manualEvent.SetAsync().ConfigureAwait(false); - } - else if (newCount < 0) - { - throw new InvalidOperationException(); - } + this.manualEvent.Set(); } - - /// - /// Decrements the counter by one. - /// - public void Signal() + else if (newCount < 0) { - int newCount = Interlocked.Decrement(ref this.remainingCount); - if (newCount == 0) - { - this.manualEvent.Set(); - } - else if (newCount < 0) - { - throw new InvalidOperationException(); - } + throw new InvalidOperationException(); } + } - /// - /// Decrements the counter by one and returns an awaitable that executes the continuation when the countdown reaches zero. - /// - /// An awaitable. - public Task SignalAndWaitAsync() + /// + /// Decrements the counter by one and returns an awaitable that executes the continuation when the countdown reaches zero. + /// + /// An awaitable. + public Task SignalAndWaitAsync() + { + try + { + this.Signal(); + return this.WaitAsync(); + } + catch (Exception ex) { - try - { - this.Signal(); - return this.WaitAsync(); - } - catch (Exception ex) - { - return Task.FromException(ex); - } + return Task.FromException(ex); } } } diff --git a/src/Microsoft.VisualStudio.Threading/AsyncCrossProcessMutex.cs b/src/Microsoft.VisualStudio.Threading/AsyncCrossProcessMutex.cs new file mode 100644 index 000000000..612656fc1 --- /dev/null +++ b/src/Microsoft.VisualStudio.Threading/AsyncCrossProcessMutex.cs @@ -0,0 +1,203 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// A mutex that can be entered asynchronously. +/// +/// +/// +/// This class utilizes the OS mutex synchronization primitive, which is fundamentally thread-affinitized and requires synchronously blocking the thread that will own the mutex. +/// This makes a native mutex unsuitable for use in async methods, where the thread that enters the mutex may not be the same thread that exits it. +/// This class solves that problem by using a private dedicated thread to enter and release the mutex, but otherwise allows its owner to execute async code, switch threads, etc. +/// +/// +/// +public class AsyncCrossProcessMutex + : IDisposable +{ +#pragma warning disable SA1310 // Field names should not contain underscore + private const int STATE_READY = 0; + private const int STATE_HELD_OR_WAITING = 1; + private const int STATE_DISPOSED = 2; +#pragma warning restore SA1310 // Field names should not contain underscore + + private static readonly Action ExitSentinel = new Action(() => { }); + private readonly Thread namedMutexOwner; + private readonly BlockingCollection mutexWorkQueue = new(); + private readonly Mutex mutex; + private int state; + + /// + /// Initializes a new instance of the class. + /// + /// + /// A non-empty name for the mutex, which follows standard mutex naming rules. + /// This name will share a namespace with other processes in the system and collisions will result in the processes sharing a single mutex across processes. + /// + /// + /// See the help docs on the underlying class for more information on the parameter. + /// Consider when reading that the initiallyOwned parameter for that constructor is always for this class. + /// + public AsyncCrossProcessMutex(string name) + { + Requires.NotNullOrEmpty(name); + this.namedMutexOwner = new Thread(this.MutexOwnerThread, 256 * 1024) + { + IsBackground = true, + Name = $"{nameof(AsyncCrossProcessMutex)}-{name}", + }; + this.mutex = new Mutex(false, name); + this.namedMutexOwner.Start(); + this.Name = name; + } + + /// + /// Gets the name of the mutex. + /// + public string Name { get; } + + /// + /// Disposes of the underlying native objects. + /// + public void Dispose() + { + int priorState = Interlocked.Exchange(ref this.state, STATE_DISPOSED); + if (priorState != STATE_DISPOSED) + { + this.mutexWorkQueue.Add(ExitSentinel); + this.mutexWorkQueue.CompleteAdding(); + } + } + + /// + public Task EnterAsync() => this.EnterAsync(Timeout.InfiniteTimeSpan); + + /// + /// Acquires the mutex asynchronously. + /// + /// The maximum time to wait before timing out. Use for no timeout, or to acquire the mutex only if it is immediately available. + /// A value whose disposal will release the mutex. + /// Thrown from the awaited result if the mutex could not be acquired within the specified timeout. + /// Thrown from the awaited result if the is a negative number other than -1 milliseconds, which represents an infinite timeout. + /// Thrown if called before a prior call to this method has completed, with its releaser disposed if the mutex was entered. + public async Task EnterAsync(TimeSpan timeout) => await this.TryEnterAsync(timeout) ?? throw new TimeoutException(); + + /// + /// Acquires the mutex asynchronously, allowing for timeouts without throwing exceptions. + /// + /// The maximum time to wait before timing out. Use for no timeout, or to acquire the mutex only if it is immediately available. + /// + /// If the mutex was acquired, the result is a value whose disposal will release the mutex. + /// In the event of a timeout, the result in a value. + /// + /// Thrown from the awaited result if the is a negative number other than -1 milliseconds, which represents an infinite timeout. + /// Thrown if called before a prior call to this method has completed, with its releaser disposed if the mutex was entered. + public Task TryEnterAsync(TimeSpan timeout) + { + int priorState = Interlocked.CompareExchange(ref this.state, STATE_HELD_OR_WAITING, STATE_READY); + switch (priorState) + { + case STATE_HELD_OR_WAITING: + throw new InvalidOperationException(); + case STATE_DISPOSED: + throw new ObjectDisposedException(this.GetType().FullName); + } + + // Pass `this` as the state simply to assist in debugging dumps. + TaskCompletionSource tcs = new(this, TaskCreationOptions.RunContinuationsAsynchronously); + this.mutexWorkQueue.Add(delegate + { + try + { + if (this.mutex.WaitOne(timeout)) + { + tcs.SetResult(new LockReleaser(this)); + } + else + { + Assumes.True(Interlocked.CompareExchange(ref this.state, STATE_READY, STATE_HELD_OR_WAITING) == STATE_HELD_OR_WAITING); + tcs.SetResult(null); + } + } + catch (AbandonedMutexException) + { + tcs.SetResult(new LockReleaser(this, abandoned: true)); + } + catch (Exception ex) + { + Assumes.True(Interlocked.CompareExchange(ref this.state, STATE_READY, STATE_HELD_OR_WAITING) == STATE_HELD_OR_WAITING); + tcs.SetException(ex); + } + }); + + return tcs.Task; + } + + private void Release() + { + Assumes.True(Interlocked.CompareExchange(ref this.state, STATE_READY, STATE_HELD_OR_WAITING) == STATE_HELD_OR_WAITING); + this.mutexWorkQueue.Add(this.mutex.ReleaseMutex); + } + + private void MutexOwnerThread() + { + try + { + while (!this.mutexWorkQueue.IsCompleted) + { + Action work = this.mutexWorkQueue.Take(); + if (work == ExitSentinel) + { + // We use an exit sentinel to avoid an exception having to be thrown and caught on disposal when we call Take() and CompleteAdding() is called. + break; + } + + work(); + } + } + finally + { + this.mutex.Dispose(); + } + } + + /// + /// The value returned from that must be disposed to release the mutex. + /// + public struct LockReleaser : IDisposable + { + private AsyncCrossProcessMutex? owner; + + internal LockReleaser(AsyncCrossProcessMutex mutex, bool abandoned = false) + { + this.owner = mutex; + this.IsAbandoned = abandoned; + } + + /// + /// Gets a value indicating whether the mutex was abandoned by its previous owner. + /// + public bool IsAbandoned { get; } + + /// + /// Releases the named mutex. + /// + public void Dispose() + { + Interlocked.Exchange(ref this.owner, null)?.Release(); + } + } +} diff --git a/src/Microsoft.VisualStudio.Threading/AsyncEventHandler.cs b/src/Microsoft.VisualStudio.Threading/AsyncEventHandler.cs index c6eaa6462..e5ef52010 100644 --- a/src/Microsoft.VisualStudio.Threading/AsyncEventHandler.cs +++ b/src/Microsoft.VisualStudio.Threading/AsyncEventHandler.cs @@ -1,28 +1,27 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Text; - using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; - /// - /// An asynchronous event handler. - /// - /// The sender of the event. - /// Event arguments. - /// A task whose completion signals handling is finished. - public delegate Task AsyncEventHandler(object? sender, EventArgs args); +namespace Microsoft.VisualStudio.Threading; - /// - /// An asynchronous event handler. - /// - /// The type of event arguments. - /// The sender of the event. - /// Event arguments. - /// A task whose completion signals handling is finished. - public delegate Task AsyncEventHandler(object? sender, TEventArgs args); -} +/// +/// An asynchronous event handler. +/// +/// The sender of the event. +/// Event arguments. +/// A task whose completion signals handling is finished. +public delegate Task AsyncEventHandler(object? sender, EventArgs args); + +/// +/// An asynchronous event handler. +/// +/// The type of event arguments. +/// The sender of the event. +/// Event arguments. +/// A task whose completion signals handling is finished. +public delegate Task AsyncEventHandler(object? sender, TEventArgs args); diff --git a/src/Microsoft.VisualStudio.Threading/AsyncLazyInitializer.cs b/src/Microsoft.VisualStudio.Threading/AsyncLazyInitializer.cs index a06092218..6e68a3610 100644 --- a/src/Microsoft.VisualStudio.Threading/AsyncLazyInitializer.cs +++ b/src/Microsoft.VisualStudio.Threading/AsyncLazyInitializer.cs @@ -1,63 +1,62 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// Lazily executes a delegate that has some side effect (typically initializing something) +/// such that the delegate runs at most once. +/// +public class AsyncLazyInitializer { - using System; - using System.Threading; - using System.Threading.Tasks; + /// + /// The lazy instance we use internally for the bulk of the behavior we want. + /// + private readonly AsyncLazy lazy; /// - /// Lazily executes a delegate that has some side effect (typically initializing something) - /// such that the delegate runs at most once. + /// Initializes a new instance of the class. /// - public class AsyncLazyInitializer + /// The action to perform at most once, that has some desirable side-effect. + /// The factory to use when invoking the in to avoid deadlocks when the main thread is required by the . + public AsyncLazyInitializer(Func action, JoinableTaskFactory? joinableTaskFactory = null) { - /// - /// The lazy instance we use internally for the bulk of the behavior we want. - /// - private readonly AsyncLazy lazy; - - /// - /// Initializes a new instance of the class. - /// - /// The action to perform at most once, that has some desirable side-effect. - /// The factory to use when invoking the in to avoid deadlocks when the main thread is required by the . - public AsyncLazyInitializer(Func action, JoinableTaskFactory? joinableTaskFactory = null) - { - Requires.NotNull(action, nameof(action)); - this.lazy = new AsyncLazy( - async delegate - { - await action().ConfigureAwaitRunInline(); - return default; - }, - joinableTaskFactory); - } - - /// - /// Gets a value indicating whether the action has executed completely, regardless of whether it threw an exception. - /// - public bool IsCompleted => this.lazy.IsValueFactoryCompleted; - - /// - /// Gets a value indicating whether the action has executed completely without throwing an exception. - /// - public bool IsCompletedSuccessfully => this.lazy.IsValueFactoryCompleted && this.lazy.GetValueAsync().Status == TaskStatus.RanToCompletion; - - /// - /// Executes the action given in the constructor if it has not yet been executed, - /// or waits for it to complete if in progress from a prior call. - /// - /// Any exception thrown by the action is rethrown here. - public void Initialize(CancellationToken cancellationToken = default) => this.lazy.GetValue(cancellationToken); - - /// - /// Executes the action given in the constructor if it has not yet been executed, - /// or waits for it to complete if in progress from a prior call. - /// - /// A task that tracks completion of the action. - /// Any exception thrown by the action is rethrown here. - public Task InitializeAsync(CancellationToken cancellationToken = default) => this.lazy.GetValueAsync(cancellationToken); + Requires.NotNull(action, nameof(action)); + this.lazy = new AsyncLazy( + async delegate + { + await action().ConfigureAwaitRunInline(); + return default; + }, + joinableTaskFactory); } + + /// + /// Gets a value indicating whether the action has executed completely, regardless of whether it threw an exception. + /// + public bool IsCompleted => this.lazy.IsValueFactoryCompleted; + + /// + /// Gets a value indicating whether the action has executed completely without throwing an exception. + /// + public bool IsCompletedSuccessfully => this.lazy.IsValueFactoryCompleted && this.lazy.GetValueAsync().Status == TaskStatus.RanToCompletion; + + /// + /// Executes the action given in the constructor if it has not yet been executed, + /// or waits for it to complete if in progress from a prior call. + /// + /// Any exception thrown by the action is rethrown here. + public void Initialize(CancellationToken cancellationToken = default) => this.lazy.GetValue(cancellationToken); + + /// + /// Executes the action given in the constructor if it has not yet been executed, + /// or waits for it to complete if in progress from a prior call. + /// + /// A task that tracks completion of the action. + /// Any exception thrown by the action is rethrown here. + public Task InitializeAsync(CancellationToken cancellationToken = default) => this.lazy.GetValueAsync(cancellationToken); } diff --git a/src/Microsoft.VisualStudio.Threading/AsyncLazy`1.cs b/src/Microsoft.VisualStudio.Threading/AsyncLazy`1.cs index e14eac66e..bbe9018e0 100644 --- a/src/Microsoft.VisualStudio.Threading/AsyncLazy`1.cs +++ b/src/Microsoft.VisualStudio.Threading/AsyncLazy`1.cs @@ -1,247 +1,518 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// A thread-safe, lazily and asynchronously evaluated value factory. +/// +/// The type of value generated by the value factory. +/// +/// This class does not itself carry any resources needful of disposing. +/// But the value factory may produce a value that needs to be disposed of, +/// which is why this class carries a method but does not implement . +/// +public class AsyncLazy { - using System; - using System.Diagnostics; - using System.Threading; - using System.Threading.Tasks; + /// + /// The value set to the field + /// while the value factory is executing. + /// + private static readonly object RecursiveCheckSentinel = new object(); /// - /// A thread-safe, lazily and asynchronously evaluated value factory. + /// A value set on the field when this object is disposed. /// - /// The type of value generated by the value factory. - public class AsyncLazy - { - /// - /// The value set to the field - /// while the value factory is executing. - /// - private static readonly object RecursiveCheckSentinel = new object(); + private static readonly Task DisposedSentinel = Task.FromException(new ObjectDisposedException(nameof(AsyncLazy))); - /// - /// The object to lock to provide thread-safety. - /// - private readonly object syncObject = new object(); + /// + /// The object to lock to provide thread-safety. + /// + private readonly object syncObject = new object(); - /// - /// The unique instance identifier. - /// - private readonly AsyncLocal recursiveFactoryCheck = new AsyncLocal(); + /// + /// An optional means to avoid deadlocks when synchronous APIs are called that must invoke async methods in user code. + /// + private readonly JoinableTaskFactory? jobFactory; - /// - /// The function to invoke to produce the task. - /// - private Func>? valueFactory; + /// + /// The unique instance identifier. + /// + private AsyncLocal? recursiveFactoryCheck; - /// - /// The async pump to Join on calls to . - /// - private JoinableTaskFactory? jobFactory; + /// + /// The function to invoke to produce the task. + /// + private Func>? valueFactory; - /// - /// The result of the value factory. - /// - private Task? value; + /// + /// The result of the value factory. + /// + private Task? value; - /// - /// A joinable task whose result is the value to be cached. - /// - private JoinableTask? joinableTask; + /// + /// A joinable task whose result is the value to be cached. + /// + private JoinableTask? joinableTask; - /// - /// Initializes a new instance of the class. - /// - /// The async function that produces the value. To be invoked at most once. - /// The factory to use when invoking the value factory in to avoid deadlocks when the main thread is required by the value factory. - public AsyncLazy(Func> valueFactory, JoinableTaskFactory? joinableTaskFactory = null) + /// + /// Initializes a new instance of the class. + /// + /// The async function that produces the value. To be invoked at most once. + /// + /// The to use for avoiding deadlocks when the + /// or the constructed value's method may require the main thread in the process. + /// + public AsyncLazy(Func> valueFactory, JoinableTaskFactory? joinableTaskFactory = null) + { + Requires.NotNull(valueFactory, nameof(valueFactory)); + this.valueFactory = valueFactory; + this.jobFactory = joinableTaskFactory; + } + + /// + /// Gets a value indicating whether to suppress detection of a value factory depending on itself. + /// + /// The default value is . + /// + /// + /// A value factory that truly depends on itself (e.g. by calling on the same instance) + /// would deadlock, and by default this class will throw an exception if it detects such a condition. + /// However this detection relies on the .NET ExecutionContext, which can flow to "spin off" contexts that are not awaited + /// by the factory, and thus could legally await the result of the value factory without deadlocking. + /// + /// + /// When this flows improperly, it can cause to be thrown, but only when the value factory + /// has not already been completed, leading to a difficult to reproduce race condition. + /// Such a case can be resolved by calling around the non-awaited fork in , + /// or the entire instance can be configured to suppress this check by setting this property to . + /// + /// + /// When this property is set to , the recursive factory check will not be performed, + /// but will still call into + /// if a was provided to the constructor. + /// + /// + public bool SuppressRecursiveFactoryDetection { get; init; } + + /// + /// Gets a value indicating whether the value factory has been invoked. + /// + /// + /// This returns after a call to . + /// + public bool IsValueCreated + { + get { - Requires.NotNull(valueFactory, nameof(valueFactory)); - this.valueFactory = valueFactory; - this.jobFactory = joinableTaskFactory; + // This is carefully written to interact well with the DisposeValueAsync method + // without requiring a lock here. + bool result = Volatile.Read(ref this.valueFactory) is null; + Interlocked.MemoryBarrier(); + result &= Volatile.Read(ref this.value) != DisposedSentinel; + return result; } + } - /// - /// Gets a value indicating whether the value factory has been invoked. - /// - public bool IsValueCreated + /// + /// Gets a value indicating whether the value factory has been invoked and has run to completion. + /// + /// + /// This returns after a call to . + /// + public bool IsValueFactoryCompleted + { + get { - get - { - Interlocked.MemoryBarrier(); - return this.valueFactory is null; - } + Task? value = Volatile.Read(ref this.value); + return value is object && value.IsCompleted && value != DisposedSentinel; } + } - /// - /// Gets a value indicating whether the value factory has been invoked and has run to completion. - /// - public bool IsValueFactoryCompleted + /// + /// Gets a value indicating whether has already been called. + /// + public bool IsValueDisposed => Volatile.Read(ref this.value) == DisposedSentinel; + + /// + /// Gets the task that produces or has produced the value. + /// + /// A task whose result is the lazily constructed value. + /// + /// Thrown when the value factory calls on this instance. + /// + /// Thrown after is called. + public Task GetValueAsync() => this.GetValueAsync(CancellationToken.None); + + /// + /// Gets the task that produces or has produced the value. + /// + /// + /// A token whose cancellation indicates that the caller no longer is interested in the result. + /// Note that this will not cancel the value factory (since other callers may exist). + /// But this token will result in an expediant cancellation of the returned Task, + /// and a dis-joining of any that may have occurred as a result of this call. + /// + /// A task whose result is the lazily constructed value. + /// + /// Thrown when the value factory calls on this instance. + /// + /// Thrown after is called. + public Task GetValueAsync(CancellationToken cancellationToken) + { + if (this.value is not { IsCompleted: true } && this.recursiveFactoryCheck is { Value: not null }) { - get - { - Interlocked.MemoryBarrier(); - return this.value is object && this.value.IsCompleted; - } + // PERF: we check the condition and *then* retrieve the string resource only on failure + // because the string retrieval has shown up as significant on ETL traces. + Verify.FailOperation(Strings.ValueFactoryReentrancy); } - /// - /// Gets the task that produces or has produced the value. - /// - /// A task whose result is the lazily constructed value. - /// - /// Thrown when the value factory calls on this instance. - /// - public Task GetValueAsync() => this.GetValueAsync(CancellationToken.None); - - /// - /// Gets the task that produces or has produced the value. - /// - /// - /// A token whose cancellation indicates that the caller no longer is interested in the result. - /// Note that this will not cancel the value factory (since other callers may exist). - /// But this token will result in an expediant cancellation of the returned Task, - /// and a dis-joining of any that may have occurred as a result of this call. - /// - /// A task whose result is the lazily constructed value. - /// - /// Thrown when the value factory calls on this instance. - /// - public Task GetValueAsync(CancellationToken cancellationToken) + if (this.value is null) { - if (!((this.value is object && this.value.IsCompleted) || this.recursiveFactoryCheck.Value is null)) + if (Monitor.IsEntered(this.syncObject)) { // PERF: we check the condition and *then* retrieve the string resource only on failure // because the string retrieval has shown up as significant on ETL traces. Verify.FailOperation(Strings.ValueFactoryReentrancy); } - if (this.value is null) + InlineResumable? resumableAwaiter = null; + lock (this.syncObject) { - if (Monitor.IsEntered(this.syncObject)) + // Note that if multiple threads hit GetValueAsync() before + // the valueFactory has completed its synchronous execution, + // then only one thread will execute the valueFactory while the + // other threads synchronously block till the synchronous portion + // has completed. + if (this.value is null) { - // PERF: we check the condition and *then* retrieve the string resource only on failure - // because the string retrieval has shown up as significant on ETL traces. - Verify.FailOperation(Strings.ValueFactoryReentrancy); - } + RoslynDebug.Assert(this.valueFactory is object); - InlineResumable? resumableAwaiter = null; - lock (this.syncObject) - { - // Note that if multiple threads hit GetValueAsync() before - // the valueFactory has completed its synchronous execution, - // then only one thread will execute the valueFactory while the - // other threads synchronously block till the synchronous portion - // has completed. - if (this.value is null) + cancellationToken.ThrowIfCancellationRequested(); + resumableAwaiter = new InlineResumable(); + Func>? originalValueFactory = this.valueFactory; + this.valueFactory = null; + Func> valueFactory = async delegate { - RoslynDebug.Assert(this.valueFactory is object); - - cancellationToken.ThrowIfCancellationRequested(); - resumableAwaiter = new InlineResumable(); - Func>? originalValueFactory = this.valueFactory; - this.valueFactory = null; - Func> valueFactory = async delegate - { - try - { - await resumableAwaiter; - return await originalValueFactory().ConfigureAwaitRunInline(); - } - finally - { - this.jobFactory = null; - this.joinableTask = null; - } - }; - - this.recursiveFactoryCheck.Value = RecursiveCheckSentinel; + Func>? localValueFactory = originalValueFactory; + originalValueFactory = null; try { - if (this.jobFactory is object) - { - // Wrapping with RunAsync allows a future caller - // to synchronously block the Main thread waiting for the result - // without leading to deadlocks. - this.joinableTask = this.jobFactory.RunAsync(valueFactory); - this.value = this.joinableTask.Task; - } - else - { - this.value = valueFactory(); - } + await resumableAwaiter; + return await localValueFactory().ConfigureAwaitRunInline(); } finally + { + localValueFactory = null; + this.joinableTask = null; + } + }; + + if (!this.SuppressRecursiveFactoryDetection) + { + Assumes.Null(this.recursiveFactoryCheck); + this.recursiveFactoryCheck = new AsyncLocal() { Value = RecursiveCheckSentinel }; + } + + try + { + if (this.jobFactory is object) + { + // Wrapping with RunAsync allows a future caller + // to synchronously block the Main thread waiting for the result + // without leading to deadlocks. + this.joinableTask = this.jobFactory.RunAsync(valueFactory); + + // this ensures that this.joinableTask must be committed before this.value + Thread.MemoryBarrier(); + + this.value = this.joinableTask.Task; + } + else + { + this.value = valueFactory(); + } + } + finally + { + if (this.recursiveFactoryCheck is not null) { this.recursiveFactoryCheck.Value = null; } } } + } + + // Allow the original value factory to actually run. + resumableAwaiter?.Resume(); + } + + // this ensures that this.joinableTask cannot be retrieved before the conditional check using this.value + Thread.MemoryBarrier(); + + return this.joinableTask?.JoinAsync(continueOnCapturedContext: false, cancellationToken) ?? this.value.WithCancellation(cancellationToken); + } + + /// + /// Gets the lazily computed value. + /// + /// The lazily constructed value. + /// + /// Thrown when the value factory calls on this instance. + /// + public T GetValue() => this.GetValue(CancellationToken.None); + + /// + /// Gets the lazily computed value. + /// + /// + /// A token whose cancellation indicates that the caller no longer is interested in the result. + /// Note that this will not cancel the value factory (since other callers may exist). + /// But when this token is canceled, the caller will experience an + /// immediately and a dis-joining of any that may have occurred as a result of this call. + /// + /// The lazily constructed value. + /// + /// Thrown when the value factory calls on this instance. + /// + /// Thrown when is canceled before the value is computed. + public T GetValue(CancellationToken cancellationToken) + { + // As a perf optimization, avoid calling JTF or GetValueAsync if the value factory has already completed. + if (this.IsValueFactoryCompleted) + { + RoslynDebug.Assert(this.value is object); + + return this.value.GetAwaiter().GetResult(); + } + else + { + return this.jobFactory is JoinableTaskFactory jtf + ? jtf.Run(() => this.GetValueAsync(cancellationToken)) + : this.GetValueAsync(cancellationToken).GetAwaiter().GetResult(); + } + } + + /// + /// Marks the code that follows as irrelevant to the receiving value factory. + /// + /// A value to dispose of to restore relevance into the value factory. + /// + /// In some cases asynchronous work may be spun off inside a value factory. + /// When the value factory does not require this work to finish before the value factory can complete, + /// it can be useful to use this method to mark that code as irrelevant to the value factory. + /// In particular, this can be necessary when the spun off task may actually include code that may itself + /// await the completion of the value factory itself. + /// Such a situation would lead to an being thrown from + /// if the value factory has not completed already, + /// which can introduce non-determinstic failures in the program. + /// A using block around the spun off code can help your program achieve reliable behavior, as shown below. + /// + /// numberOfApples; + /// + /// public MyClass() { + /// this.numberOfApples = new AsyncLazy(async delegate { + /// // We have some fire-and-forget code to run. + /// // This is *not* relevant to the value factory, which is allowed to complete without waiting for this code to finish. + /// using (this.numberOfApples.SuppressRelevance()) { + /// this.FireOffNotificationsAsync(); + /// } + /// + /// // This code is relevant to the value factory, and must complete before the value factory can complete. + /// return await this.CountNumberOfApplesAsync(); + /// }); + /// } + /// + /// public event EventHandler? ApplesCountingHasBegun; + /// + /// public async Task GetApplesCountAsync(CancellationToken cancellationToken) { + /// return await this.numberOfApples.GetValueAsync(cancellationToken); + /// } + /// + /// private async Task CountNumberOfApplesAsync() { + /// await Task.Delay(1000); + /// return 5; + /// } + /// + /// private async Task FireOffNotificationsAsync() { + /// // This may call to 3rd party code, which may happen to call back into GetApplesCountAsync (and thus into our AsyncLazy instance), + /// // but such calls should *not* be interpreted as value factory reentrancy. They should just wait for the value factory to finish. + /// // We accomplish this by suppressing relevance of the value factory while this code runs (see the caller of this method above). + /// this.ApplesCountingHasBegun?.Invoke(this, EventArgs.Empty); + /// } + /// } + /// ]]> + /// + /// If the was created with a , + /// this method also calls on the + /// associated with that factory. + /// + /// + public RevertRelevance SuppressRelevance() => new RevertRelevance(this); + + /// + /// Disposes of the lazily-initialized value if disposable, and causes all subsequent attempts to obtain the value to fail. + /// + /// + /// This call will block on disposal (which may include construction of the value itself if it has already started but not yet finished) if it is the first call to dispose of the value. + /// Calling this method will put this object into a disposed state where future calls to obtain the value will throw . + /// If the value has already been produced and implements or , it will be disposed of. + /// If the value factory has already started but has not yet completed, its value will be disposed of when the value factory completes. + /// If prior calls to obtain the value are in flight when this method is called, those calls may complete and their callers may obtain the value, although + /// may have been or will soon be called on the value, leading those users to experience a . + /// Note all conditions based on the value implementing or is based on the actual value, rather than the type argument. + /// This means that although may be IFoo (which does not implement ), the concrete type that implements IFoo may implement + /// and thus be treated as a disposable object as described above. + /// + public void DisposeValue() + { + if (!this.IsValueDisposed) + { + if (this.jobFactory is JoinableTaskFactory jtf) + { + jtf.Run(this.DisposeValueAsync); + } + else + { + this.DisposeValueAsync().GetAwaiter().GetResult(); + } + } + } - // Allow the original value factory to actually run. - resumableAwaiter?.Resume(); + /// + /// Disposes of the lazily-initialized value if disposable, and causes all subsequent attempts to obtain the value to fail. + /// + /// + /// A task that completes when the value has been disposed of, or immediately if the value has already been disposed of or has been scheduled for disposal by a prior call. + /// + /// + /// Calling this method will put this object into a disposed state where future calls to obtain the value will throw . + /// If the value has already been produced and implements , , or it will be disposed of. + /// If the value factory has already started but has not yet completed, its value will be disposed of when the value factory completes. + /// If prior calls to obtain the value are in flight when this method is called, those calls may complete and their callers may obtain the value, although + /// may have been or will soon be called on the value, leading those users to experience a . + /// Note all conditions based on the value implementing or is based on the actual value, rather than the type argument. + /// This means that although may be IFoo (which does not implement ), the concrete type that implements IFoo may implement + /// and thus be treated as a disposable object as described above. + /// + public async Task DisposeValueAsync() + { + JoinableTask? localJoinableTask = null; + Task? localValueTask = null; + object? localValue = default; + lock (this.syncObject) + { + if (this.value == DisposedSentinel) + { + return; } - if (!this.value.IsCompleted) + switch (this.value?.Status) { - this.joinableTask?.JoinAsync(cancellationToken).Forget(); + case TaskStatus.RanToCompletion: + // We'll dispose of the value inline, outside the lock. + localValue = this.value.Result; + break; + case TaskStatus.Faulted: + case TaskStatus.Canceled: + // Nothing left to do. + break; + default: + // We'll schedule the value for disposal outside the lock so it can be synchronous with the value factory, + // but will not execute within our lock. + localValueTask = this.value; + localJoinableTask = this.joinableTask; + break; } - return this.value.WithCancellation(cancellationToken); + // Shut out all future callers from obtaining the value. + this.value = DisposedSentinel; + + // We want value to be set before valueFactory is cleared so that IsValueCreated never returns true incorrectly. + Interlocked.MemoryBarrier(); + + // Release associated memory. + this.joinableTask = null; + this.valueFactory = null; } - /// - /// Gets the lazily computed value. - /// - /// The lazily constructed value. - /// - /// Thrown when the value factory calls on this instance. - /// - public T GetValue() => this.GetValue(CancellationToken.None); + if (localJoinableTask is not null) + { + localValue = await localJoinableTask; + } + else if (localValueTask is not null) + { + localValue = await localValueTask.ConfigureAwait(false); + } + + if (localValue is System.IAsyncDisposable systemAsyncDisposable) + { + await systemAsyncDisposable.DisposeAsync().ConfigureAwait(false); + } + else if (localValue is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync().ConfigureAwait(false); + } + else if (localValue is IDisposable disposable) + { + disposable.Dispose(); + } + } + + /// + /// Renders a string describing an uncreated value, or the string representation of the created value. + /// + public override string ToString() + { + return (this.value is object && this.value.IsCompleted) + ? (this.value.Status == TaskStatus.RanToCompletion ? $"{this.value.Result}" : Strings.LazyValueFaulted) + : Strings.LazyValueNotCreated; + } + + /// + /// A structure that hides relevance of a block of code from a particular and the it was created with. + /// + public readonly struct RevertRelevance : IDisposable + { + private readonly AsyncLazy? owner; + private readonly object? oldCheckValue; + private readonly JoinableTaskContext.RevertRelevance? joinableRelevance; /// - /// Gets the lazily computed value. + /// Initializes a new instance of the struct. /// - /// - /// A token whose cancellation indicates that the caller no longer is interested in the result. - /// Note that this will not cancel the value factory (since other callers may exist). - /// But when this token is canceled, the caller will experience an - /// immediately and a dis-joining of any that may have occurred as a result of this call. - /// - /// The lazily constructed value. - /// - /// Thrown when the value factory calls on this instance. - /// - /// Thrown when is canceled before the value is computed. - public T GetValue(CancellationToken cancellationToken) + /// The instance that created this value. + internal RevertRelevance(AsyncLazy owner) { - // As a perf optimization, avoid calling JTF or GetValueAsync if the value factory has already completed. - if (this.IsValueFactoryCompleted) - { - RoslynDebug.Assert(this.value is object); + Requires.NotNull(owner, nameof(owner)); + this.owner = owner; - return this.value.GetAwaiter().GetResult(); - } - else + if (owner.recursiveFactoryCheck is not null) { - // Capture the factory as a local before comparing and dereferencing it since - // the field can transition to null and we want to gracefully handle that race condition. - JoinableTaskFactory? factory = this.jobFactory; - return factory is object - ? factory.Run(() => this.GetValueAsync(cancellationToken)) - : this.GetValueAsync(cancellationToken).GetAwaiter().GetResult(); + (this.oldCheckValue, owner.recursiveFactoryCheck.Value) = (owner.recursiveFactoryCheck.Value, null); } + + this.joinableRelevance = owner.jobFactory?.Context.SuppressRelevance(); } /// - /// Renders a string describing an uncreated value, or the string representation of the created value. + /// Reverts the async local and thread static values to their original values. /// - public override string ToString() + public void Dispose() { - return (this.value is object && this.value.IsCompleted) - ? (this.value.Status == TaskStatus.RanToCompletion ? $"{this.value.Result}" : Strings.LazyValueFaulted) - : Strings.LazyValueNotCreated; + if (this.owner?.recursiveFactoryCheck is { } check) + { + check.Value = this.oldCheckValue; + } + + this.joinableRelevance?.Dispose(); } } } diff --git a/src/Microsoft.VisualStudio.Threading/AsyncLocal`1.cs b/src/Microsoft.VisualStudio.Threading/AsyncLocal`1.cs index 5956a7fd4..31c7126a2 100644 --- a/src/Microsoft.VisualStudio.Threading/AsyncLocal`1.cs +++ b/src/Microsoft.VisualStudio.Threading/AsyncLocal`1.cs @@ -1,36 +1,35 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +namespace Microsoft.VisualStudio.Threading; + +/// +/// Stores references such that they are available for retrieval +/// in the same call context. +/// +/// The type of value to store. +public partial class AsyncLocal + where T : class { /// - /// Stores references such that they are available for retrieval - /// in the same call context. + /// The framework version specific instance of AsyncLocal to use. /// - /// The type of value to store. - public partial class AsyncLocal - where T : class - { - /// - /// The framework version specific instance of AsyncLocal to use. - /// - private readonly System.Threading.AsyncLocal asyncLocal; + private readonly System.Threading.AsyncLocal asyncLocal; - /// - /// Initializes a new instance of the class. - /// - public AsyncLocal() - { - this.asyncLocal = new System.Threading.AsyncLocal(); - } + /// + /// Initializes a new instance of the class. + /// + public AsyncLocal() + { + this.asyncLocal = new System.Threading.AsyncLocal(); + } - /// - /// Gets or sets the value to associate with the current CallContext. - /// - public T? Value - { - get { return this.asyncLocal.Value; } - set { this.asyncLocal.Value = value; } - } + /// + /// Gets or sets the value to associate with the current CallContext. + /// + public T? Value + { + get { return this.asyncLocal.Value; } + set { this.asyncLocal.Value = value; } } } diff --git a/src/Microsoft.VisualStudio.Threading/AsyncManualResetEvent.cs b/src/Microsoft.VisualStudio.Threading/AsyncManualResetEvent.cs index 94a04c424..10f081c03 100644 --- a/src/Microsoft.VisualStudio.Threading/AsyncManualResetEvent.cs +++ b/src/Microsoft.VisualStudio.Threading/AsyncManualResetEvent.cs @@ -1,243 +1,182 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// A flavor of that can be asynchronously awaited on. +/// +[DebuggerDisplay("Signaled: {IsSet}")] +public class AsyncManualResetEvent { - using System; - using System.Collections.Generic; - using System.ComponentModel; - using System.Diagnostics; - using System.Linq; - using System.Runtime.CompilerServices; - using System.Text; - using System.Threading; - using System.Threading.Tasks; + /// + /// Options to use while creating the to return from . + /// + private readonly TaskCreationOptions options; /// - /// A flavor of that can be asynchronously awaited on. + /// The object to lock when accessing fields. /// - [DebuggerDisplay("Signaled: {IsSet}")] - public class AsyncManualResetEvent - { - /// - /// Whether the task completion source should allow executing continuations synchronously. - /// - private readonly bool allowInliningAwaiters; - - /// - /// The object to lock when accessing fields. - /// - private readonly object syncObject = new object(); - - /// - /// The source of the task to return from . - /// - /// - /// This should not need the volatile modifier because it is - /// always accessed within a lock. - /// - private TaskCompletionSourceWithoutInlining taskCompletionSource; - - /// - /// A flag indicating whether the event is signaled. - /// When this is set to true, it's possible that - /// .Task.IsCompleted is still false - /// if the completion has been scheduled asynchronously. - /// Thus, this field should be the definitive answer as to whether - /// the event is signaled because it is synchronously updated. - /// - /// - /// This should not need the volatile modifier because it is - /// always accessed within a lock. - /// - private bool isSet; - - /// - /// Initializes a new instance of the class. - /// - /// A value indicating whether the event should be initially signaled. - /// - /// A value indicating whether to allow callers' continuations to execute - /// on the thread that calls before the call returns. - /// callers should not hold private locks if this value is true to avoid deadlocks. - /// When false, the task returned from may not have fully transitioned to - /// its completed state by the time returns to its caller. - /// - public AsyncManualResetEvent(bool initialState = false, bool allowInliningAwaiters = false) - { - this.allowInliningAwaiters = allowInliningAwaiters; + private readonly object syncObject = new object(); - this.taskCompletionSource = this.CreateTaskSource(); - this.isSet = initialState; - if (initialState) - { - this.taskCompletionSource.SetResult(EmptyStruct.Instance); - } - } + /// + /// The source of the task to return from . + /// + /// + /// This should not need the volatile modifier because it is + /// always accessed within a lock. + /// + private TaskCompletionSource taskCompletionSource; + + /// + /// Initializes a new instance of the class. + /// + /// A value indicating whether the event should be initially signaled. + /// + /// A value indicating whether to allow callers' continuations to execute + /// on the thread that calls before the call returns. + /// callers should not hold private locks if this value is to avoid deadlocks. + /// + public AsyncManualResetEvent(bool initialState = false, bool allowInliningAwaiters = false) + { + this.options = allowInliningAwaiters ? TaskCreationOptions.None : TaskCreationOptions.RunContinuationsAsynchronously; - /// - /// Gets a value indicating whether the event is currently in a signaled state. - /// - public bool IsSet + this.taskCompletionSource = new(this.options); + if (initialState) { - get - { - lock (this.syncObject) - { - return this.isSet; - } - } + this.taskCompletionSource.SetResult(EmptyStruct.Instance); } + } - /// - /// Returns a task that will be completed when this event is set. - /// - public Task WaitAsync() + /// + /// Gets a value indicating whether the event is currently in a signaled state. + /// + public bool IsSet + { + get { lock (this.syncObject) { - return this.taskCompletionSource.Task; + return this.taskCompletionSource.Task.IsCompleted; } } + } - /// - /// Returns a task that will be completed when this event is set. - /// - /// A cancellation token. - /// A task that completes when the event is set, or cancels with the . - public Task WaitAsync(CancellationToken cancellationToken) => this.WaitAsync().WithCancellation(cancellationToken); - - /// - /// Sets this event to unblock callers of . - /// - /// A task that completes when the signal has been set. - /// - /// - /// On .NET versions prior to 4.6: - /// This method may return before the signal set has propagated (so may return false for a bit more if called immediately). - /// The returned task completes when the signal has definitely been set. - /// - /// - /// On .NET 4.6 and later: - /// This method is not asynchronous. The returned Task is always completed. - /// - /// - [Obsolete("Use Set() instead."), EditorBrowsable(EditorBrowsableState.Never)] - public Task SetAsync() + /// + /// Returns a task that will be completed when this event is set. + /// + public Task WaitAsync() + { + lock (this.syncObject) { - TaskCompletionSourceWithoutInlining? tcs = null; - bool transitionRequired = false; - lock (this.syncObject) - { - transitionRequired = !this.isSet; - tcs = this.taskCompletionSource; - this.isSet = true; - } - - // Snap the Task that is exposed to the outside so we return that one. - // Once we complete the TaskCompletionSourceWithoutInlinining's task, - // the Task property will return the inner Task. - // SetAsync should return the same Task that WaitAsync callers would have observed previously. - Task result = tcs.Task; + return this.taskCompletionSource.Task; + } + } - if (transitionRequired) - { - tcs.TrySetResult(default(EmptyStruct)); - } + /// + /// Returns a task that will be completed when this event is set. + /// + /// A cancellation token. + /// A task that completes when the event is set, or cancels with the . + public Task WaitAsync(CancellationToken cancellationToken) => this.WaitAsync().WithCancellation(cancellationToken); - return result; + /// + /// Sets this event to unblock callers of . + /// + /// A task that completes when the signal has been set. + /// + /// This method is not asynchronous. The returned Task is always completed. + /// + [Obsolete("Use Set() instead."), EditorBrowsable(EditorBrowsableState.Never)] + public Task SetAsync() + { + TaskCompletionSource? tcs = null; + lock (this.syncObject) + { + tcs = this.taskCompletionSource; } - /// - /// Sets this event to unblock callers of . - /// - public void Set() - { + tcs.TrySetResult(default); + + // SetAsync should return the same Task that WaitAsync callers would have observed previously. + return tcs.Task; + } + + /// + /// Sets this event to unblock callers of . + /// + public void Set() + { #pragma warning disable CS0618 // Type or member is obsolete - this.SetAsync(); + this.SetAsync(); #pragma warning restore CS0618 // Type or member is obsolete - } + } - /// - /// Resets this event to a state that will block callers of . - /// - public void Reset() + /// + /// Resets this event to a state that will block callers of . + /// + public void Reset() + { + lock (this.syncObject) { - lock (this.syncObject) + if (this.taskCompletionSource.Task.IsCompleted) { - if (this.isSet) - { - this.taskCompletionSource = this.CreateTaskSource(); - this.isSet = false; - } + this.taskCompletionSource = new(this.options); } } + } - /// - /// Sets and immediately resets this event, allowing all current waiters to unblock. - /// - /// A task that completes when the signal has been set. - /// - /// - /// On .NET versions prior to 4.6: - /// This method may return before the signal set has propagated (so may return false for a bit more if called immediately). - /// The returned task completes when the signal has definitely been set. - /// - /// - /// On .NET 4.6 and later: - /// This method is not asynchronous. The returned Task is always completed. - /// - /// - [Obsolete("Use PulseAll() instead."), EditorBrowsable(EditorBrowsableState.Never)] - public Task PulseAllAsync() + /// + /// Sets and immediately resets this event, allowing all current waiters to unblock. + /// + /// A task that completes when the signal has been set. + /// + /// This method is not asynchronous. The returned Task is always completed. + /// + [Obsolete("Use PulseAll() instead."), EditorBrowsable(EditorBrowsableState.Never)] + public Task PulseAllAsync() + { + TaskCompletionSource? tcs = null; + lock (this.syncObject) { - TaskCompletionSourceWithoutInlining? tcs = null; - lock (this.syncObject) - { - // Atomically replace the completion source with a new, uncompleted source - // while capturing the previous one so we can complete it. - // This ensures that we don't leave a gap in time where WaitAsync() will - // continue to return completed Tasks due to a Pulse method which should - // execute instantaneously. - tcs = this.taskCompletionSource; - this.taskCompletionSource = this.CreateTaskSource(); - this.isSet = false; - } - - // Snap the Task that is exposed to the outside so we return that one. - // Once we complete the TaskCompletionSourceWithoutInlinining's task, - // the Task property will return the inner Task. - // PulseAllAsync should return the same Task that WaitAsync callers would have observed previously. - Task result = tcs.Task; - tcs.TrySetResult(default(EmptyStruct)); - return result; + // Atomically replace the completion source with a new, uncompleted source + // while capturing the previous one so we can complete it. + // This ensures that we don't leave a gap in time where WaitAsync() will + // continue to return completed Tasks due to a Pulse method which should + // execute instantaneously. + tcs = this.taskCompletionSource; + this.taskCompletionSource = new(this.options); } - /// - /// Sets and immediately resets this event, allowing all current waiters to unblock. - /// - public void PulseAll() - { + tcs.TrySetResult(default); + + // PulseAllAsync should return the same Task that WaitAsync callers would have observed previously. + return tcs.Task; + } + + /// + /// Sets and immediately resets this event, allowing all current waiters to unblock. + /// + public void PulseAll() + { #pragma warning disable CS0618 // Type or member is obsolete - this.PulseAllAsync(); + this.PulseAllAsync(); #pragma warning restore CS0618 // Type or member is obsolete - } - - /// - /// Gets an awaiter that completes when this event is signaled. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public TaskAwaiter GetAwaiter() - { - return this.WaitAsync().GetAwaiter(); - } + } - /// - /// Creates a new TaskCompletionSource to represent an unset event. - /// - private TaskCompletionSourceWithoutInlining CreateTaskSource() - { - return new TaskCompletionSourceWithoutInlining(this.allowInliningAwaiters); - } + /// + /// Gets an awaiter that completes when this event is signaled. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public TaskAwaiter GetAwaiter() + { + return this.WaitAsync().GetAwaiter(); } } diff --git a/src/Microsoft.VisualStudio.Threading/AsyncQueue`1.cs b/src/Microsoft.VisualStudio.Threading/AsyncQueue`1.cs index fea615b3f..61affeb04 100644 --- a/src/Microsoft.VisualStudio.Threading/AsyncQueue`1.cs +++ b/src/Microsoft.VisualStudio.Threading/AsyncQueue`1.cs @@ -1,458 +1,461 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// A thread-safe, asynchronously dequeuable queue. +/// +/// The type of values kept by the queue. +[DebuggerDisplay("Count = {Count}, Completed = {completeSignaled}")] +public class AsyncQueue : ThreadingTools.ICancellationNotification { - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Diagnostics.CodeAnalysis; - using System.Linq; - using System.Text; - using System.Threading; - using System.Threading.Tasks; + /// + /// The source of the task returned by . Lazily constructed. + /// + /// + /// Volatile to allow the check-lock-check pattern in to be reliable, + /// in the event that within the lock, one thread initializes the value and assigns the field + /// and the weak memory model allows the assignment prior to the initialization. Another thread + /// outside the lock might observe the non-null field and start accessing the Task property + /// before it is actually initialized. Volatile prevents CPU reordering of commands around + /// the assignment (or read) of this field. + /// + private volatile TaskCompletionSource? completedSource; + + /// + /// The internal queue of elements. Lazily constructed. + /// + private Queue? queueElements; + + /// + /// The internal queue of waiters. Lazily constructed. + /// + private Queue>? dequeuingWaiters; + + /// + /// A value indicating whether has been called. + /// + private bool completeSignaled; + + /// + /// A flag indicating whether the has been invoked. + /// + private bool onCompletedInvoked; /// - /// A thread-safe, asynchronously dequeuable queue. + /// Initializes a new instance of the class. /// - /// The type of values kept by the queue. - [DebuggerDisplay("Count = {Count}, Completed = {completeSignaled}")] - public class AsyncQueue : ThreadingTools.ICancellationNotification + public AsyncQueue() { - /// - /// The source of the task returned by . Lazily constructed. - /// - /// - /// Volatile to allow the check-lock-check pattern in to be reliable, - /// in the event that within the lock, one thread initializes the value and assigns the field - /// and the weak memory model allows the assignment prior to the initialization. Another thread - /// outside the lock might observe the non-null field and start accessing the Task property - /// before it is actually initialized. Volatile prevents CPU reordering of commands around - /// the assignment (or read) of this field. - /// - private volatile TaskCompletionSource? completedSource; - - /// - /// The internal queue of elements. Lazily constructed. - /// - private Queue? queueElements; - - /// - /// The internal queue of waiters. Lazily constructed. - /// - private Queue>? dequeuingWaiters; - - /// - /// A value indicating whether has been called. - /// - private bool completeSignaled; - - /// - /// A flag indicating whether the has been invoked. - /// - private bool onCompletedInvoked; - - /// - /// Initializes a new instance of the class. - /// - public AsyncQueue() - { - } + } - /// - /// Gets a value indicating whether the queue is currently empty. - /// - public bool IsEmpty - { - get { return this.Count == 0; } - } + /// + /// Gets a value indicating whether the queue is currently empty. + /// + public bool IsEmpty + { + get { return this.Count == 0; } + } - /// - /// Gets the number of elements currently in the queue. - /// - public int Count + /// + /// Gets the number of elements currently in the queue. + /// + public int Count + { + get { - get + lock (this.SyncRoot) { - lock (this.SyncRoot) - { - return this.queueElements?.Count ?? 0; - } + return this.queueElements?.Count ?? 0; } } + } - /// - /// Gets a value indicating whether the queue has completed. - /// - /// - /// This is arguably redundant with .IsCompleted, but this property - /// won't cause the lazy instantiation of the Task that may if there - /// is no other reason for the Task to exist. - /// - public bool IsCompleted + /// + /// Gets a value indicating whether the queue is both empty and had invoked. + /// + /// + /// This is arguably redundant with .IsCompleted, but this property + /// won't cause the lazy instantiation of the Task that may if there + /// is no other reason for the Task to exist. + /// + public bool IsCompleted + { + get { - get + lock (this.SyncRoot) { - lock (this.SyncRoot) - { - return this.completeSignaled && this.IsEmpty; - } + return this.completeSignaled && this.IsEmpty; } } + } - /// - /// Gets a task that transitions to a completed state when is called. - /// - public Task Completion + /// + /// Gets a task that transitions to a completed state when is called and the queue is empty. + /// + public Task Completion + { + get { - get + if (this.completedSource is null) { - if (this.completedSource is null) + lock (this.SyncRoot) { - lock (this.SyncRoot) + if (this.completedSource is null) { - if (this.completedSource is null) + if (this.IsCompleted) + { + return Task.CompletedTask; + } + else { - if (this.IsCompleted) - { - return Task.CompletedTask; - } - else - { - this.completedSource = new TaskCompletionSource(); - } + this.completedSource = new TaskCompletionSource(); } } } - - return this.completedSource.Task; } + + return this.completedSource.Task; } + } - /// - /// Gets the synchronization object used by this queue. - /// - protected object SyncRoot => this; // save allocations by using this instead of a new object. + /// + /// Gets the synchronization object used by this queue. + /// + protected object SyncRoot => this; // save allocations by using this instead of a new object. - /// - /// Gets the initial capacity for the queue. - /// - protected virtual int InitialCapacity => 4; + /// + /// Gets the initial capacity for the queue. + /// + protected virtual int InitialCapacity => 4; - /// - /// Signals that no further elements will be enqueued. - /// - public void Complete() + /// + /// Signals that no further elements will be enqueued. + /// + /// + /// This method will return immediately. + /// Elements enqueued before calling this method may still be dequeued. + /// will return true only after this method has been called and the queue is empty. + /// + public void Complete() + { + lock (this.SyncRoot) { - lock (this.SyncRoot) - { - this.completeSignaled = true; - } - - this.CompleteIfNecessary(); + this.completeSignaled = true; } - /// - /// Adds an element to the tail of the queue. - /// - /// The value to add. - public void Enqueue(T value) + this.CompleteIfNecessary(); + } + + /// + /// Adds an element to the tail of the queue. + /// + /// The value to add. + /// Thrown if has already been called. Use to avoid an exception in this case. + public void Enqueue(T value) + { + if (!this.TryEnqueue(value)) { - if (!this.TryEnqueue(value)) - { - Verify.FailOperation(Strings.InvalidAfterCompleted); - } + Verify.FailOperation(Strings.InvalidAfterCompleted); } + } - /// - /// Adds an element to the tail of the queue if it has not yet completed. - /// - /// The value to add. - /// true if the value was added to the queue; false if the queue is already completed. - public bool TryEnqueue(T value) + /// + /// Adds an element to the tail of the queue if it has not yet completed. + /// + /// The value to add. + /// if the value was added to the queue; if the queue is already completed. + public bool TryEnqueue(T value) + { + bool alreadyDispatched = false; + lock (this.SyncRoot) { - bool alreadyDispatched = false; - lock (this.SyncRoot) + if (this.completeSignaled) { - if (this.completeSignaled) - { - return false; - } + return false; + } - // Is a dequeuer waiting for this? - while (this.dequeuingWaiters?.Count > 0) + // Is a dequeuer waiting for this? + while (this.dequeuingWaiters?.Count > 0) + { + TaskCompletionSource waitingDequeuer = this.dequeuingWaiters.Dequeue(); + if (waitingDequeuer.TrySetResult(value)) { - TaskCompletionSource waitingDequeuer = this.dequeuingWaiters.Dequeue(); - if (waitingDequeuer.TrySetResult(value)) - { - alreadyDispatched = true; - break; - } + alreadyDispatched = true; + break; } + } - this.FreeCanceledDequeuers(); + this.FreeCanceledDequeuers(); - if (!alreadyDispatched) + if (!alreadyDispatched) + { + if (this.queueElements is null) { - if (this.queueElements is null) - { - this.queueElements = new Queue(this.InitialCapacity); - } - - this.queueElements.Enqueue(value); + this.queueElements = new Queue(this.InitialCapacity); } + + this.queueElements.Enqueue(value); } + } - this.OnEnqueued(value, alreadyDispatched); + this.OnEnqueued(value, alreadyDispatched); - return true; - } + return true; + } - /// - /// Gets the value at the head of the queue without removing it from the queue, if it is non-empty. - /// - /// Receives the value at the head of the queue; or the default value for the element type if the queue is empty. - /// true if the queue was non-empty; false otherwise. - public bool TryPeek([MaybeNullWhen(false)] out T value) + /// + /// Gets the value at the head of the queue without removing it from the queue, if it is non-empty. + /// + /// Receives the value at the head of the queue; or the default value for the element type if the queue is empty. + /// if the queue was non-empty; otherwise. + public bool TryPeek([MaybeNullWhen(false)] out T value) + { + lock (this.SyncRoot) { - lock (this.SyncRoot) + if (this.queueElements is object && this.queueElements.Count > 0) { - if (this.queueElements is object && this.queueElements.Count > 0) - { - value = this.queueElements.Peek(); - return true; - } - else - { - value = default(T)!; - return false; - } + value = this.queueElements.Peek(); + return true; + } + else + { + value = default(T)!; + return false; } } + } - /// - /// Gets the value at the head of the queue without removing it from the queue. - /// - /// Thrown if the queue is empty. - public T Peek() - { + /// + /// Gets the value at the head of the queue without removing it from the queue. + /// + /// Thrown if the queue is empty. + public T Peek() + { #pragma warning disable CS8717 // A member returning a [MaybeNull] value introduces a null value for a type parameter. - if (!this.TryPeek(out T? value)) + if (!this.TryPeek(out T? value)) #pragma warning restore CS8717 // A member returning a [MaybeNull] value introduces a null value for a type parameter. - { - Verify.FailOperation(Strings.QueueEmpty); - } + { + Verify.FailOperation(Strings.QueueEmpty); + } - return value; + return value; + } + + /// + /// Gets a task whose result is the element at the head of the queue. + /// + /// + /// A token whose cancellation signals lost interest in the item. + /// Cancelling this token does *not* guarantee that the task will be canceled + /// before it is assigned a resulting element from the head of the queue. + /// It is the responsibility of the caller to ensure after cancellation that + /// either the task is canceled, or it has a result which the caller is responsible + /// for then handling. + /// + /// A task whose result is the head element. + /// + /// Thrown when this instance has an empty queue and has been called. + /// Also thrown when is canceled before a work item can be dequeued. + /// + public Task DequeueAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); } - /// - /// Gets a task whose result is the element at the head of the queue. - /// - /// - /// A token whose cancellation signals lost interest in the item. - /// Cancelling this token does *not* guarantee that the task will be canceled - /// before it is assigned a resulting element from the head of the queue. - /// It is the responsibility of the caller to ensure after cancellation that - /// either the task is canceled, or it has a result which the caller is responsible - /// for then handling. - /// - /// A task whose result is the head element. - /// - /// Thrown when this instance has an empty queue and has been called. - /// Also thrown when is canceled before a work item can be dequeued. - /// - public Task DequeueAsync(CancellationToken cancellationToken = default(CancellationToken)) + T result; + lock (this.SyncRoot) { - if (cancellationToken.IsCancellationRequested) + if (this.IsCompleted) { - return Task.FromCanceled(cancellationToken); + return TplExtensions.CanceledTaskOfT(); } - T result; - lock (this.SyncRoot) + if (this.queueElements?.Count > 0) { - if (this.IsCompleted) - { - return TplExtensions.CanceledTaskOfT(); - } - - if (this.queueElements?.Count > 0) + result = this.queueElements.Dequeue(); + } + else + { + if (this.dequeuingWaiters is null) { - result = this.queueElements.Dequeue(); + this.dequeuingWaiters = new Queue>(capacity: 2); } else { - if (this.dequeuingWaiters is null) - { - this.dequeuingWaiters = new Queue>(capacity: 2); - } - else - { - this.FreeCanceledDequeuers(); - } - - var waiterTcs = new TaskCompletionSourceWithoutInlining(allowInliningContinuations: false); - waiterTcs.AttachCancellation(cancellationToken, this); - this.dequeuingWaiters.Enqueue(waiterTcs); - return waiterTcs.Task; + this.FreeCanceledDequeuers(); } - } - this.CompleteIfNecessary(); - return Task.FromResult(result); + var waiterTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + waiterTcs.AttachCancellation(cancellationToken, this); + this.dequeuingWaiters.Enqueue(waiterTcs); + return waiterTcs.Task; + } } - /// - /// Immediately dequeues the element from the head of the queue if one is available, - /// otherwise returns without an element. - /// - /// Receives the element from the head of the queue; or default(T) if the queue is empty. - /// true if an element was dequeued; false if the queue was empty. - public bool TryDequeue([MaybeNullWhen(false)] out T value) - { + this.CompleteIfNecessary(); + return Task.FromResult(result); + } + + /// + /// Immediately dequeues the element from the head of the queue if one is available, + /// otherwise returns without an element. + /// + /// Receives the element from the head of the queue; or default(T) if the queue is empty. + /// if an element was dequeued; if the queue was empty. + public bool TryDequeue([MaybeNullWhen(false)] out T value) + { #pragma warning disable CS8717 // A member returning a [MaybeNull] value introduces a null value for a type parameter. - bool result = this.TryDequeueInternal(null, out value); + bool result = this.TryDequeueInternal(null, out value); #pragma warning restore CS8717 // A member returning a [MaybeNull] value introduces a null value for a type parameter. - this.CompleteIfNecessary(); - return result; - } + this.CompleteIfNecessary(); + return result; + } - /// - void ThreadingTools.ICancellationNotification.OnCanceled() => this.FreeCanceledDequeuers(); + /// + void ThreadingTools.ICancellationNotification.OnCanceled() => this.FreeCanceledDequeuers(); - /// - /// Returns a copy of this queue as an array. - /// - internal T[] ToArray() + /// + /// Returns a copy of this queue as an array. + /// + public T[] ToArray() + { + lock (this.SyncRoot) { - lock (this.SyncRoot) - { - return this.queueElements?.ToArray() ?? Array.Empty(); - } + return this.queueElements?.ToArray() ?? Array.Empty(); } + } - /// - /// Immediately dequeues the element from the head of the queue if one is available - /// that satisfies the specified check; - /// otherwise returns without an element. - /// - /// The test on the head element that must succeed to dequeue. - /// Receives the element from the head of the queue; or default(T) if the queue is empty. - /// true if an element was dequeued; false if the queue was empty. - protected bool TryDequeue(Predicate valueCheck, [MaybeNullWhen(false)] out T value) - { - Requires.NotNull(valueCheck, nameof(valueCheck)); + /// + /// Immediately dequeues the element from the head of the queue if one is available + /// that satisfies the specified check; + /// otherwise returns without an element. + /// + /// The test on the head element that must succeed to dequeue. + /// Receives the element from the head of the queue; or default(T) if the queue is empty. + /// if an element was dequeued; if the queue was empty. + protected bool TryDequeue(Predicate valueCheck, [MaybeNullWhen(false)] out T value) + { + Requires.NotNull(valueCheck, nameof(valueCheck)); #pragma warning disable CS8717 // A member returning a [MaybeNull] value introduces a null value for a type parameter. - bool result = this.TryDequeueInternal(valueCheck, out value); + bool result = this.TryDequeueInternal(valueCheck, out value); #pragma warning restore CS8717 // A member returning a [MaybeNull] value introduces a null value for a type parameter. - this.CompleteIfNecessary(); - return result; - } + this.CompleteIfNecessary(); + return result; + } - /// - /// Invoked when a value is enqueued. - /// - /// The enqueued value. - /// - /// true if the item will skip the queue because a dequeuer was already waiting for an item; - /// false if the item was actually added to the queue. - /// - protected virtual void OnEnqueued(T value, bool alreadyDispatched) - { - } + /// + /// Invoked when a value is enqueued. + /// + /// The enqueued value. + /// + /// if the item will skip the queue because a dequeuer was already waiting for an item; + /// if the item was actually added to the queue. + /// + protected virtual void OnEnqueued(T value, bool alreadyDispatched) + { + } - /// - /// Invoked when a value is dequeued. - /// - /// The dequeued value. - protected virtual void OnDequeued(T value) - { - } + /// + /// Invoked when a value is dequeued. + /// + /// The dequeued value. + protected virtual void OnDequeued(T value) + { + } - /// - /// Invoked when the queue is completed. - /// - protected virtual void OnCompleted() - { - } + /// + /// Invoked when the queue is completed. + /// + protected virtual void OnCompleted() + { + } - /// - /// Immediately dequeues the element from the head of the queue if one is available, - /// otherwise returns without an element. - /// - /// The test on the head element that must succeed to dequeue. - /// Receives the element from the head of the queue; or default(T) if the queue is empty. - /// true if an element was dequeued; false if the queue was empty. - private bool TryDequeueInternal(Predicate? valueCheck, [MaybeNullWhen(false)] out T value) + /// + /// Immediately dequeues the element from the head of the queue if one is available, + /// otherwise returns without an element. + /// + /// The test on the head element that must succeed to dequeue. + /// Receives the element from the head of the queue; or default(T) if the queue is empty. + /// if an element was dequeued; if the queue was empty. + private bool TryDequeueInternal(Predicate? valueCheck, [MaybeNullWhen(false)] out T value) + { + bool dequeued; + lock (this.SyncRoot) { - bool dequeued; - lock (this.SyncRoot) + if (this.queueElements is object && this.queueElements.Count > 0 && (valueCheck is null || valueCheck(this.queueElements.Peek()))) { - if (this.queueElements is object && this.queueElements.Count > 0 && (valueCheck is null || valueCheck(this.queueElements.Peek()))) - { - value = this.queueElements.Dequeue(); - dequeued = true; - } - else - { - value = default(T)!; - dequeued = false; - } + value = this.queueElements.Dequeue(); + dequeued = true; } - - if (dequeued) + else { - this.OnDequeued(value); + value = default(T)!; + dequeued = false; } - - return dequeued; } - /// - /// Transitions this queue to a completed state if signaled and the queue is empty. - /// - private void CompleteIfNecessary() + if (dequeued) { - Assumes.False(Monitor.IsEntered(this.SyncRoot)); // important because we'll transition a task to complete. + this.OnDequeued(value); + } - bool transitionTaskSource, invokeOnCompleted = false; - lock (this.SyncRoot) + return dequeued; + } + + /// + /// Transitions this queue to a completed state if signaled and the queue is empty. + /// + private void CompleteIfNecessary() + { + Assumes.False(Monitor.IsEntered(this.SyncRoot)); // important because we'll transition a task to complete. + + bool transitionTaskSource, invokeOnCompleted = false; + lock (this.SyncRoot) + { + transitionTaskSource = this.completeSignaled && (this.queueElements is null || this.queueElements.Count == 0); + if (transitionTaskSource) { - transitionTaskSource = this.completeSignaled && (this.queueElements is null || this.queueElements.Count == 0); - if (transitionTaskSource) + invokeOnCompleted = !this.onCompletedInvoked; + this.onCompletedInvoked = true; + while (this.dequeuingWaiters?.Count > 0) { - invokeOnCompleted = !this.onCompletedInvoked; - this.onCompletedInvoked = true; - while (this.dequeuingWaiters?.Count > 0) - { - this.dequeuingWaiters.Dequeue().TrySetCanceled(); - } + this.dequeuingWaiters.Dequeue().TrySetCanceled(); } } + } - if (transitionTaskSource) + if (transitionTaskSource) + { + this.completedSource?.TrySetResult(null); + if (invokeOnCompleted) { - this.completedSource?.TrySetResult(null); - if (invokeOnCompleted) - { - this.OnCompleted(); - } + this.OnCompleted(); } } + } - /// - /// Clears as many canceled dequeuers as we can from the head of the waiting queue. - /// - private void FreeCanceledDequeuers() + /// + /// Clears as many canceled dequeuers as we can from the head of the waiting queue. + /// + private void FreeCanceledDequeuers() + { + lock (this.SyncRoot) { - lock (this.SyncRoot) + while (this.dequeuingWaiters?.Count > 0 && this.dequeuingWaiters.Peek().Task.IsCompleted) { - while (this.dequeuingWaiters?.Count > 0 && this.dequeuingWaiters.Peek().Task.IsCompleted) - { - this.dequeuingWaiters.Dequeue(); - } + this.dequeuingWaiters.Dequeue(); } } } diff --git a/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterLock+HangReportContributor.cs b/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterLock+HangReportContributor.cs index 9fe19cf16..08f4ffcbf 100644 --- a/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterLock+HangReportContributor.cs +++ b/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterLock+HangReportContributor.cs @@ -1,17 +1,17 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading; +using System.Xml.Linq; + namespace Microsoft.VisualStudio.Threading { - using System; - using System.Collections.Generic; - using System.Globalization; - using System.Linq; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - using System.Xml.Linq; - public partial class AsyncReaderWriterLock : IHangReportContributor { [Flags] @@ -46,6 +46,7 @@ protected internal virtual SynchronizationContext NoMessagePumpSynchronizationCo /// Contributes data for a hang report. /// /// The hang report contribution. Null values should be ignored. + [RequiresUnreferencedCode(Reasons.DiagnosticAnalysisOnly)] HangReportContribution IHangReportContributor.GetHangReport() { return this.GetHangReport(); @@ -55,7 +56,8 @@ HangReportContribution IHangReportContributor.GetHangReport() /// Contributes data for a hang report. /// /// The hang report contribution. Null values should be ignored. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity"), SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] + [RequiresUnreferencedCode(Reasons.DiagnosticAnalysisOnly)] protected virtual HangReportContribution GetHangReport() { using (this.NoMessagePumpSynchronizationContext.Apply()) @@ -125,6 +127,7 @@ private static XDocument CreateDgml(out XElement nodes, out XElement links) /// /// Appends details of a given collection of awaiters to the hang report. /// + [RequiresUnreferencedCode(Reasons.DiagnosticAnalysisOnly)] private static XElement CreateAwaiterNode(Awaiter awaiter) { Requires.NotNull(awaiter, nameof(awaiter)); @@ -211,9 +214,13 @@ public IEnumerable Categories { get { +#if NET + foreach (AwaiterCollection value in Enum.GetValues()) +#else #pragma warning disable CS8605 // Unboxing a possibly null value. foreach (AwaiterCollection value in Enum.GetValues(typeof(AwaiterCollection))) #pragma warning restore CS8605 // Unboxing a possibly null value. +#endif { if (this.Membership.HasFlag(value)) { diff --git a/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterLock.cs b/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterLock.cs index ca1f5604c..bbdefb950 100644 --- a/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterLock.cs +++ b/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterLock.cs @@ -1,2781 +1,2864 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// A non-blocking lock that allows concurrent access, exclusive access, or concurrent with upgradeability to exclusive access. +/// +/// +/// We have to use a custom awaitable rather than simply returning Task{LockReleaser} because +/// we have to set CallContext data in the context of the person receiving the lock, +/// which requires that we get to execute code at the start of the continuation (whether we yield or not). +/// +/// +/// Considering this class to be a state machine, the states are: +/// +/// READERS +/// | IDLE | <-----> UPGRADEABLE READER + READERS -----> UPGRADED WRITER --\ +/// | NO LOCKS | ^ | +/// | | |--- RE-ENTER CONCURRENCY PREP <--/ +/// | | <-----> WRITER +/// ------------- +/// ]]> +/// +/// +public partial class AsyncReaderWriterLock : IDisposable { - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Diagnostics.CodeAnalysis; - using System.Globalization; - using System.Linq; - using System.Runtime.CompilerServices; - using System.Threading; - using System.Threading.Tasks; + /// + /// A time delay to check whether pending writer lock and reader locks forms a deadlock. + /// + private static readonly TimeSpan DefaultDeadlockCheckTimeout = TimeSpan.FromSeconds(3); + + /// + /// The default SynchronizationContext to schedule work after issuing a lock. + /// + private static readonly SynchronizationContext DefaultSynchronizationContext = new SynchronizationContext(); + + /// + /// The object to acquire a Monitor-style lock on for all field access on this instance. + /// + private readonly object syncObject = new object(); + + /// + /// A JoinableTaskContext used to resolve dependencies between read locks to lead into deadlocks when there is a pending write lock. + /// + private readonly JoinableTaskContext? joinableTaskContext; + + /// + /// A CallContext-local reference to the Awaiter that is on the top of the stack (most recently acquired). + /// + private readonly AsyncLocal topAwaiter = new AsyncLocal(); + + /// + /// The set of read locks that are issued and active. + /// + /// + /// Many readers are allowed concurrently. Also, readers may re-enter read locks (recursively) + /// each of which gets an element in this set. + /// + private readonly HashSet issuedReadLocks = new HashSet(); + + /// + /// The set of upgradeable read locks that are issued and active. + /// + /// + /// Although only one upgradeable read lock can be held at a time, this set may have more + /// than one element because that one lock holder may enter the lock it already possesses + /// multiple times. + /// + private readonly HashSet issuedUpgradeableReadLocks = new HashSet(); + + /// + /// The set of write locks that are issued and active. + /// + /// + /// Although only one write lock can be held at a time, this set may have more + /// than one element because that one lock holder may enter the lock it already possesses + /// multiple times. + /// Although this lock is mutually exclusive, there *may* be elements in the + /// set if the write lock was upgraded from a reader. + /// Also note that some elements in this may themselves be upgradeable readers if they have + /// the flag. + /// + private readonly HashSet issuedWriteLocks = new HashSet(); + + /// + /// A queue of readers waiting to obtain the concurrent read lock. + /// + private readonly Queue waitingReaders = new Queue(); + + /// + /// A queue of upgradeable readers waiting to obtain a lock. + /// + private readonly Queue waitingUpgradeableReaders = new Queue(); + + /// + /// A queue of writers waiting to obtain an exclusive lock. + /// + private readonly Queue waitingWriters = new Queue(); + + /// + /// The source of the task, which transitions to completed after + /// the method is called and all issued locks have been released. + /// + private readonly TaskCompletionSource completionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); /// - /// A non-blocking lock that allows concurrent access, exclusive access, or concurrent with upgradeability to exclusive access. + /// The queue of callbacks to invoke when the currently held write lock is totally released. /// /// - /// We have to use a custom awaitable rather than simply returning Task{LockReleaser} because - /// we have to set CallContext data in the context of the person receiving the lock, - /// which requires that we get to execute code at the start of the continuation (whether we yield or not). + /// If the write lock is released to an upgradeable read lock, these callbacks are fired synchronously + /// with respect to the writer who is releasing the lock. Otherwise, the callbacks are invoked + /// asynchronously with respect to the releasing thread. /// - /// - /// Considering this class to be a state machine, the states are: - /// - /// READERS - /// | IDLE | <-----> UPGRADEABLE READER + READERS -----> UPGRADED WRITER --\ - /// | NO LOCKS | ^ | - /// | | |--- RE-ENTER CONCURRENCY PREP <--/ - /// | | <-----> WRITER - /// ------------- - /// ]]> - /// - /// - public partial class AsyncReaderWriterLock : IDisposable + private readonly Queue> beforeWriteReleasedCallbacks = new Queue>(); + + /// + /// A helper class to produce ETW trace events. + /// + private readonly EventsHelper etw; + + /// + /// A value indicating whether extra resources should be spent to collect diagnostic information + /// that may be useful in deadlock investigations. + /// + private bool captureDiagnostics; + + /// + /// A flag indicating whether we're currently running code to prepare for re-entering concurrency mode + /// after releasing an exclusive lock. The Awaiter being released is the non-null value. + /// + private volatile Awaiter? reenterConcurrencyPrepRunning; + + /// + /// A flag indicating that the method has been called, indicating that no + /// new top-level lock requests should be serviced. + /// + private bool completeInvoked; + + /// + /// A timer to recheck potential deadlock caused by pending writer locks. + /// + private Timer? pendingWriterLockDeadlockCheckTimer; + + /// + /// Initializes a new instance of the class. + /// + public AsyncReaderWriterLock() + : this(joinableTaskContext: null, captureDiagnostics: false) { - /// - /// A time delay to check whether pending writer lock and reader locks forms a deadlock. - /// - private static readonly TimeSpan DefaultDeadlockCheckTimeout = TimeSpan.FromSeconds(3); + } - /// - /// The default SynchronizationContext to schedule work after issuing a lock. - /// - private static readonly SynchronizationContext DefaultSynchronizationContext = new SynchronizationContext(); + /// + /// Initializes a new instance of the class. + /// + /// + /// to spend additional resources capturing diagnostic details that can be used + /// to analyze deadlocks or other issues. + public AsyncReaderWriterLock(bool captureDiagnostics) + : this(joinableTaskContext: null, captureDiagnostics) + { + } - /// - /// The object to acquire a Monitor-style lock on for all field access on this instance. - /// - private readonly object syncObject = new object(); + /// + /// Initializes a new instance of the class. + /// + /// + /// A JoinableTaskContext to help resolve deadlocks caused by interdependency between top read lock tasks when there is a pending write lock blocking one of them. + /// + /// + /// to spend additional resources capturing diagnostic details that can be used + /// to analyze deadlocks or other issues. + public AsyncReaderWriterLock(JoinableTaskContext? joinableTaskContext, bool captureDiagnostics = false) + { + this.etw = new EventsHelper(this); - /// - /// A JoinableTaskContext used to resolve dependencies between read locks to lead into deadlocks when there is a pending write lock. - /// - private readonly JoinableTaskContext? joinableTaskContext; + this.joinableTaskContext = joinableTaskContext; + this.captureDiagnostics = captureDiagnostics; + } + /// + /// Flags that modify default lock behavior. + /// + [Flags] + public enum LockFlags + { /// - /// A CallContext-local reference to the Awaiter that is on the top of the stack (most recently acquired). + /// The default behavior applies. /// - private readonly AsyncLocal topAwaiter = new AsyncLocal(); + None = 0x0, /// - /// The set of read locks that are issued and active. + /// Causes an upgradeable reader to remain in an upgraded-write state once upgraded, + /// even after the nested write lock has been released. /// /// - /// Many readers are allowed concurrently. Also, readers may re-enter read locks (recursively) - /// each of which gets an element in this set. + /// This is useful when you have a batch of possible write operations to apply, which + /// may or may not actually apply in the end, but if any of them change anything, + /// all of their changes should be seen atomically (within a single write lock). + /// This approach is preferable to simply acquiring a write lock around the batch of + /// potential changes because it doesn't defeat concurrent readers until it knows there + /// is a change to actually make. /// - private readonly HashSet issuedReadLocks = new HashSet(); + StickyWrite = 0x1, + } + /// + /// An enumeration of the kinds of locks supported by this class. + /// + internal enum LockKind + { /// - /// The set of upgradeable read locks that are issued and active. + /// A lock that supports concurrently executing threads that hold this same lock type. + /// Holders of this lock may not obtain a lock without first + /// releasing all their locks. /// - /// - /// Although only one upgradeable read lock can be held at a time, this set may have more - /// than one element because that one lock holder may enter the lock it already possesses - /// multiple times. - /// - private readonly HashSet issuedUpgradeableReadLocks = new HashSet(); + Read, /// - /// The set of write locks that are issued and active. + /// A lock that may run concurrently with standard readers, but is exclusive of any other + /// upgradeable readers. Holders of this lock are allowed to obtain a write lock while + /// holding this lock to guarantee continuity of state between what they read and what they write. /// - /// - /// Although only one write lock can be held at a time, this set may have more - /// than one element because that one lock holder may enter the lock it already possesses - /// multiple times. - /// Although this lock is mutually exclusive, there *may* be elements in the - /// set if the write lock was upgraded from a reader. - /// Also note that some elements in this may themselves be upgradeable readers if they have - /// the flag. - /// - private readonly HashSet issuedWriteLocks = new HashSet(); + UpgradeableRead, /// - /// A queue of readers waiting to obtain the concurrent read lock. + /// A mutually exclusive lock. /// - private readonly Queue waitingReaders = new Queue(); + Write, + } - /// - /// A queue of upgradeable readers waiting to obtain a lock. - /// - private readonly Queue waitingUpgradeableReaders = new Queue(); + /// + /// Gets a value indicating whether any kind of lock is held by the caller and can + /// be immediately used given the caller's context. + /// + public bool IsAnyLockHeld + { + get { return this.IsReadLockHeld || this.IsUpgradeableReadLockHeld || this.IsWriteLockHeld; } + } - /// - /// A queue of writers waiting to obtain an exclusive lock. - /// - private readonly Queue waitingWriters = new Queue(); + /// + /// Gets a value indicating whether any kind of lock is held by the caller without regard + /// to the lock compatibility of the caller's context. + /// + public bool IsAnyPassiveLockHeld + { + get { return this.IsPassiveReadLockHeld || this.IsPassiveUpgradeableReadLockHeld || this.IsPassiveWriteLockHeld; } + } - /// - /// The source of the task, which transitions to completed after - /// the method is called and all issued locks have been released. - /// - private readonly TaskCompletionSource completionSource = new TaskCompletionSource(); + /// + /// Gets a value indicating whether the caller holds a read lock. + /// + /// + /// This property returns if any other lock type is held, unless + /// within that alternate lock type this lock is also nested. + /// + public bool IsReadLockHeld + { + get { return this.IsLockHeld(LockKind.Read); } + } - /// - /// The queue of callbacks to invoke when the currently held write lock is totally released. - /// - /// - /// If the write lock is released to an upgradeable read lock, these callbacks are fired synchronously - /// with respect to the writer who is releasing the lock. Otherwise, the callbacks are invoked - /// asynchronously with respect to the releasing thread. - /// - private readonly Queue> beforeWriteReleasedCallbacks = new Queue>(); + /// + /// Gets a value indicating whether a read lock is held by the caller without regard + /// to the lock compatibility of the caller's context. + /// + /// + /// This property returns if any other lock type is held, unless + /// within that alternate lock type this lock is also nested. + /// + public bool IsPassiveReadLockHeld + { + get { return this.IsLockHeld(LockKind.Read, checkSyncContextCompatibility: false, allowNonLockSupportingContext: true); } + } - /// - /// A value indicating whether extra resources should be spent to collect diagnostic information - /// that may be useful in deadlock investigations. - /// - private bool captureDiagnostics; + /// + /// Gets a value indicating whether the caller holds an upgradeable read lock. + /// + /// + /// This property returns if any other lock type is held, unless + /// within that alternate lock type this lock is also nested. + /// + public bool IsUpgradeableReadLockHeld + { + get { return this.IsLockHeld(LockKind.UpgradeableRead); } + } - /// - /// A flag indicating whether we're currently running code to prepare for re-entering concurrency mode - /// after releasing an exclusive lock. The Awaiter being released is the non-null value. - /// - private volatile Awaiter? reenterConcurrencyPrepRunning; + /// + /// Gets a value indicating whether an upgradeable read lock is held by the caller without regard + /// to the lock compatibility of the caller's context. + /// + /// + /// This property returns if any other lock type is held, unless + /// within that alternate lock type this lock is also nested. + /// + public bool IsPassiveUpgradeableReadLockHeld + { + get { return this.IsLockHeld(LockKind.UpgradeableRead, checkSyncContextCompatibility: false, allowNonLockSupportingContext: true); } + } - /// - /// A flag indicating that the method has been called, indicating that no - /// new top-level lock requests should be serviced. - /// - private bool completeInvoked; + /// + /// Gets a value indicating whether the caller holds a write lock. + /// + /// + /// This property returns if any other lock type is held, unless + /// within that alternate lock type this lock is also nested. + /// + public bool IsWriteLockHeld + { + get { return this.IsLockHeld(LockKind.Write); } + } - /// - /// A helper class to produce ETW trace events. - /// - private EventsHelper etw; + /// + /// Gets a value indicating whether a write lock is held by the caller without regard + /// to the lock compatibility of the caller's context. + /// + /// + /// This property returns if any other lock type is held, unless + /// within that alternate lock type this lock is also nested. + /// + public bool IsPassiveWriteLockHeld + { + get { return this.IsLockHeld(LockKind.Write, checkSyncContextCompatibility: false, allowNonLockSupportingContext: true); } + } - /// - /// A timer to recheck potential deadlock caused by pending writer locks. - /// - private Timer? pendingWriterLockDeadlockCheckTimer; + /// + /// Gets a task whose completion signals that this lock will no longer issue locks. + /// + /// + /// This task only transitions to a complete state after a call to . + /// + public Task Completion + { + get { return this.completionSource.Task; } + } - /// - /// Initializes a new instance of the class. - /// - public AsyncReaderWriterLock() - : this(joinableTaskContext: null, captureDiagnostics: false) + /// + /// Gets the object used to synchronize access to this instance's fields. + /// + protected object SyncObject + { + get { return this.syncObject; } + } + + /// + /// Gets the lock held by the caller's execution context. + /// + protected LockHandle AmbientLock + { + get { return new LockHandle(this.GetFirstActiveSelfOrAncestor(this.topAwaiter.Value)); } + } + + /// + /// Gets or sets a value indicating whether additional resources should be spent to collect + /// information that would be useful in diagnosing deadlocks, etc. + /// + protected bool CaptureDiagnostics + { + get { return this.captureDiagnostics; } + set { this.captureDiagnostics = value; } + } + + /// + /// Gets a time delay to check whether pending writer lock and reader locks forms a deadlock. + /// + protected virtual TimeSpan DeadlockCheckTimeout => DefaultDeadlockCheckTimeout; + + /// + /// Gets a value indicating whether the current thread is allowed to + /// hold an active lock. + /// + /// + /// The default implementation of this property returns + /// when the calling thread is NOT an STA thread. + /// This property may be overridden to return + /// on threads that may compromise the integrity of the lock. + /// + protected virtual bool CanCurrentThreadHoldActiveLock + { + get { return Thread.CurrentThread.GetApartmentState() != ApartmentState.STA; } + } + + /// + /// Gets a value indicating whether the current SynchronizationContext is one that is not supported + /// by this lock. + /// + protected virtual bool IsUnsupportedSynchronizationContext + { + get { + SynchronizationContext? ctxt = SynchronizationContext.Current; + bool supported = ctxt is null || ctxt is NonConcurrentSynchronizationContext; + return !supported; } + } + + /// + /// Obtains a read lock, asynchronously awaiting for the lock if it is not immediately available. + /// + /// + /// A token whose cancellation indicates lost interest in obtaining the lock. + /// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available, + /// a canceled token will cause the code that is waiting for the lock to resume with an . + /// + /// An awaitable object whose result is the lock releaser. + /// Thrown when has been called and this is a new top-level lock request. + public Awaitable ReadLockAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + return new Awaitable(this, LockKind.Read, LockFlags.None, cancellationToken); + } + + /// + /// Obtains an upgradeable read lock, asynchronously awaiting for the lock if it is not immediately available. + /// + /// + /// A token whose cancellation indicates lost interest in obtaining the lock. + /// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available, + /// a canceled token will cause the code that is waiting for the lock to resume with an . + /// + /// An awaitable object whose result is the lock releaser. + /// Thrown when has been called and this is a new top-level lock request. + public Awaitable UpgradeableReadLockAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + return new Awaitable(this, LockKind.UpgradeableRead, LockFlags.None, cancellationToken); + } + + /// + /// Obtains an upgradeable read lock, asynchronously awaiting for the lock if it is not immediately available. + /// + /// Modifications to normal lock behavior. + /// + /// A token whose cancellation indicates lost interest in obtaining the lock. + /// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available, + /// a canceled token will cause the code that is waiting for the lock to resume with an . + /// + /// An awaitable object whose result is the lock releaser. + /// Thrown when has been called and this is a new top-level lock request. + public Awaitable UpgradeableReadLockAsync(LockFlags options, CancellationToken cancellationToken = default(CancellationToken)) + { + return new Awaitable(this, LockKind.UpgradeableRead, options, cancellationToken); + } + + /// + /// Obtains a write lock, asynchronously awaiting for the lock if it is not immediately available. + /// + /// + /// A token whose cancellation indicates lost interest in obtaining the lock. + /// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available, + /// a canceled token will cause the code that is waiting for the lock to resume with an . + /// + /// An awaitable object whose result is the lock releaser. + /// Thrown when has been called and this is a new top-level lock request. + public Awaitable WriteLockAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + return new Awaitable(this, LockKind.Write, LockFlags.None, cancellationToken); + } + + /// + /// Obtains a write lock, asynchronously awaiting for the lock if it is not immediately available. + /// + /// Modifications to normal lock behavior. + /// + /// A token whose cancellation indicates lost interest in obtaining the lock. + /// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available, + /// a canceled token will cause the code that is waiting for the lock to resume with an . + /// + /// An awaitable object whose result is the lock releaser. + /// Thrown when has been called and this is a new top-level lock request. + public Awaitable WriteLockAsync(LockFlags options, CancellationToken cancellationToken = default(CancellationToken)) + { + return new Awaitable(this, LockKind.Write, options, cancellationToken); + } + + /// + /// Prevents use or visibility of the caller's lock(s) until the returned value is disposed. + /// + /// The value to dispose to restore lock visibility. + /// + /// This can be used by a write lock holder that is about to fork execution to avoid + /// two threads simultaneously believing they hold the exclusive write lock. + /// The lock should be hidden just before kicking off the work and can be restored immediately + /// after kicking off the work. + /// + public Suppression HideLocks() + { + return new Suppression(this); + } + + /// + /// Causes new top-level lock requests to be rejected and the task to transition + /// to a completed state after any issued locks have been released. + /// + public void Complete() + { + lock (this.syncObject) + { + this.completeInvoked = true; + this.CompleteIfAppropriate(); + } + } + + /// + /// Registers a callback to be invoked when the write lock held by the caller is + /// about to be ultimately released (outermost write lock). + /// + /// + /// The asynchronous delegate to invoke. + /// Access to the write lock is provided throughout the asynchronous invocation. + /// + /// + /// This supports some scenarios VC++ has where change event handlers need to inspect changes, + /// or follow up with other changes to respond to earlier changes, at the conclusion of the lock. + /// This method is safe to call from within a previously registered callback, in which case the + /// registered callback will run when previously registered callbacks have completed execution. + /// If the write lock is released to an upgradeable read lock, these callbacks are fired synchronously + /// with respect to the writer who is releasing the lock. Otherwise, the callbacks are invoked + /// asynchronously with respect to the releasing thread. + /// + public void OnBeforeWriteLockReleased(Func action) + { + Requires.NotNull(action, nameof(action)); + + lock (this.syncObject) + { + if (!this.IsWriteLockHeld) + { + throw new InvalidOperationException(); + } + + this.beforeWriteReleasedCallbacks.Enqueue(action); + } + } + + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes managed and unmanaged resources held by this instance. + /// + /// if was called; if the object is being finalized. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + Timer? timerToDispose = null; + + lock (this.syncObject) + { + timerToDispose = this.pendingWriterLockDeadlockCheckTimer; + this.pendingWriterLockDeadlockCheckTimer = null; + } + + timerToDispose?.Dispose(); + } + } + + /// + /// Checks whether the aggregated flags from all locks in the lock stack satisfy the specified flag(s). + /// + /// The flag(s) that must be specified for a result. + /// The head of the lock stack to consider. + /// if all the specified flags are found somewhere in the lock stack; otherwise. + protected bool LockStackContains(LockFlags flags, LockHandle handle) + { + LockFlags aggregateFlags = LockFlags.None; + Awaiter? awaiter = handle.Awaiter; + if (awaiter is object) + { + lock (this.syncObject) + { + while (awaiter is object) + { + if (this.IsLockActive(awaiter, considerStaActive: true, checkSyncContextCompatibility: true)) + { + aggregateFlags |= awaiter.Options; + if ((aggregateFlags & flags) == flags) + { + return true; + } + } + + awaiter = awaiter.NestingLock; + } + } + } + + return (aggregateFlags & flags) == flags; + } + + /// + /// Returns the aggregate of the lock flags for all nested locks. + /// + /// + /// This is not redundant with because that returns fast + /// once the presence of certain flag(s) is determined, whereas this will aggregate all flags, + /// some of which may be defined by derived types. + /// + protected LockFlags GetAggregateLockFlags() + { + LockFlags aggregateFlags = LockFlags.None; + Awaiter? awaiter = this.topAwaiter.Value; + if (awaiter is object) + { + lock (this.syncObject) + { + while (awaiter is object) + { + if (this.IsLockActive(awaiter, considerStaActive: true, checkSyncContextCompatibility: true)) + { + aggregateFlags |= awaiter.Options; + } + + awaiter = awaiter.NestingLock; + } + } + } + + return aggregateFlags; + } + + /// + /// Fired when any lock is being released. + /// + /// if the last write lock that the caller holds is being released; otherwise. + /// The lock being released. + /// A task whose completion signals the conclusion of the asynchronous operation. + protected virtual Task OnBeforeLockReleasedAsync(bool exclusiveLockRelease, LockHandle releasingLock) + { + // Raise the write release lock event if and only if this is the last write that is about to be released. + // Also check that issued read lock count is 0, because these callbacks themselves may acquire read locks + // on top of this write lock that hasn't quite gone away yet, and when they release their read lock, + // that shouldn't trigger a recursive call of the event. + if (exclusiveLockRelease) + { + return this.OnBeforeExclusiveLockReleasedAsync(); + } + else + { + return Task.CompletedTask; + } + } + + /// + /// Fired when the last write lock is about to be released. + /// + /// A task whose completion signals the conclusion of the asynchronous operation. + protected virtual Task OnBeforeExclusiveLockReleasedAsync() + { + lock (this.SyncObject) + { + // While this method is called when the last write lock is about to be released, + // a derived type may override this method and have already taken an additional write lock, + // so only state our assumption in the non-derivation case. + Assumes.True(this.issuedWriteLocks.Count == 1 || !this.GetType().Equals(typeof(AsyncReaderWriterLock))); + + if (this.beforeWriteReleasedCallbacks.Count > 0) + { + return this.InvokeBeforeWriteLockReleaseHandlersAsync(); + } + else + { + return Task.CompletedTask; + } + } + } + + /// + /// Get the task scheduler to execute the continuation when the lock is acquired. + /// AsyncReaderWriterLock uses a special to handle exclusive locks, and will ignore task scheduler provided, so this is only used in a read lock scenario. + /// This method is called within the execution context to wait the read lock, so it can pick up based on the current execution context. + /// Note: the task scheduler is only used, when the lock is issued later. If the lock is issued immediately when returns true, it will be ignored. + /// + /// A task scheduler to schedule the continuation task when a lock is issued. + protected virtual TaskScheduler GetTaskSchedulerForReadLockRequest() + { + return TaskScheduler.Default; + } + + /// + /// Invoked after an exclusive lock is released but before anyone has a chance to enter the lock. + /// + /// + /// This method is called while holding a private lock in order to block future lock consumers till this method is finished. + /// + protected virtual Task OnExclusiveLockReleasedAsync() + { + return Task.CompletedTask; + } + + /// + /// Invoked when a top-level upgradeable read lock is released, leaving no remaining (write) lock. + /// + protected virtual void OnUpgradeableReadLockReleased() + { + } + + /// + /// Invoked when a nested lock request is detected from a forked execution context + /// of an upgradeable read lock holder — specifically, when the calling thread could hold + /// an active lock (it is not an STA thread) but lacks the required + /// . + /// + /// + /// This typically indicates that code holding an upgradeable read lock used + /// ConfigureAwait(false) or otherwise left the , + /// then requested a nested read lock on the forked context. + /// This method is called outside the lock's private synchronization object. + /// Implementations should be lightweight (e.g., setting a flag or posting a telemetry event). + /// The default implementation does nothing. Override to log diagnostics or telemetry. + /// + protected virtual void OnLockForkDetected() + { + } + + /// + /// Invoked when the lock detects an internal error or illegal usage pattern that + /// indicates a serious flaw that should be immediately reported to the application + /// and/or bring down the process to avoid hangs or data corruption. + /// + /// The exception that captures the details of the failure. + /// An exception that may be returned by some implementations of tis method for he caller to rethrow. + protected virtual Exception OnCriticalFailure(Exception ex) + { + Requires.NotNull(ex, nameof(ex)); + + Report.Fail(ex.Message); + Environment.FailFast(ex.ToString(), ex); + throw Assumes.NotReachable(); + } + + /// + /// Invoked when the lock detects an internal error or illegal usage pattern that + /// indicates a serious flaw that should be immediately reported to the application + /// and/or bring down the process to avoid hangs or data corruption. + /// + /// The message to use for the exception. + /// An exception that may be returned by some implementations of tis method for he caller to rethrow. + protected Exception OnCriticalFailure(string message) + { + try + { + throw Assumes.Fail(message); + } + catch (Exception ex) + { + throw this.OnCriticalFailure(ex); + } + } + + /// + /// Checks whether the specified lock has any active nested locks. + /// + private static bool HasAnyNestedLocks(Awaiter lck, HashSet lockCollection) + { + Requires.NotNull(lck, nameof(lck)); + Requires.NotNull(lockCollection, nameof(lockCollection)); + + if (lockCollection.Count > 0) + { + foreach (Awaiter? nestedCandidate in lockCollection) + { + if (nestedCandidate == lck) + { + // This isn't nested -- it's the lock itself. + continue; + } + + for (Awaiter? a = nestedCandidate.NestingLock; a is object; a = a.NestingLock) + { + if (a == lck) + { + return true; + } + } + } + } + + return false; + } + + private static void PendingWriterLockDeadlockWatchingCallback(object? state) + { + var readerWriterLock = (AsyncReaderWriterLock?)state; + Assumes.NotNull(readerWriterLock); + + readerWriterLock.TryInvokeAllDependentReadersIfAppropriate(); + + lock (readerWriterLock.syncObject) + { + readerWriterLock.pendingWriterLockDeadlockCheckTimer?.Change((int)readerWriterLock.DeadlockCheckTimeout.TotalMilliseconds, -1); + } + } + + /// + /// Throws an exception if called on an STA thread. + /// + private void ThrowIfUnsupportedThreadOrSyncContext() + { + if (!this.CanCurrentThreadHoldActiveLock) + { + Verify.FailOperation(Strings.STAThreadCallerNotAllowed); + } + + if (this.IsUnsupportedSynchronizationContext) + { + Verify.FailOperation(Strings.AppliedSynchronizationContextNotAllowed); + } + } + + /// + /// Gets a value indicating whether the caller's thread apartment model and SynchronizationContext + /// is compatible with a lock. + /// + private bool IsLockSupportingContext(Awaiter? awaiter = null) + { + if (!this.CanCurrentThreadHoldActiveLock || this.IsUnsupportedSynchronizationContext) + { + return false; + } + + awaiter = awaiter ?? this.topAwaiter.Value; + if (this.IsLockHeld(LockKind.Write, awaiter, allowNonLockSupportingContext: true, checkSyncContextCompatibility: false) || + this.IsLockHeld(LockKind.UpgradeableRead, awaiter, allowNonLockSupportingContext: true, checkSyncContextCompatibility: false)) + { + if (!(SynchronizationContext.Current is NonConcurrentSynchronizationContext)) + { + // Upgradeable read and write locks *must* have the NonConcurrentSynchronizationContext applied. + return false; + } + } + + return true; + } + + /// + /// Transitions the task to a completed state + /// if appropriate. + /// + private void CompleteIfAppropriate() + { + Assumes.True(Monitor.IsEntered(this.syncObject)); - /// - /// Initializes a new instance of the class. - /// - /// - /// true to spend additional resources capturing diagnostic details that can be used - /// to analyze deadlocks or other issues. - public AsyncReaderWriterLock(bool captureDiagnostics) - : this(joinableTaskContext: null, captureDiagnostics) + if (this.completeInvoked && + !this.completionSource.Task.IsCompleted && + this.reenterConcurrencyPrepRunning is null && + this.issuedReadLocks.Count == 0 && this.issuedUpgradeableReadLocks.Count == 0 && this.issuedWriteLocks.Count == 0 && + this.waitingReaders.Count == 0 && this.waitingUpgradeableReaders.Count == 0 && this.waitingWriters.Count == 0) { + this.completionSource.TrySetResult(null); } + } - /// - /// Initializes a new instance of the class. - /// - /// - /// A JoinableTaskContext to help resolve deadlocks caused by interdependency between top read lock tasks when there is a pending write lock blocking one of them. - /// - /// - /// true to spend additional resources capturing diagnostic details that can be used - /// to analyze deadlocks or other issues. - public AsyncReaderWriterLock(JoinableTaskContext? joinableTaskContext, bool captureDiagnostics = false) + /// + /// Detects which lock types the given lock holder has (including all nested locks). + /// + /// The most nested lock to be considered. + /// Receives a value indicating whether a read lock is held. + /// Receives a value indicating whether an upgradeable read lock is held. + /// Receives a value indicating whether a write lock is held. + private void AggregateLockStackKinds(Awaiter? awaiter, out bool read, out bool upgradeableRead, out bool write) + { + read = false; + upgradeableRead = false; + write = false; + + if (awaiter is object) { - this.etw = new EventsHelper(this); + lock (this.syncObject) + { + while (awaiter is object) + { + // It's possible that this lock has been released (even mid-stack, due to our async nature), + // so only consider locks that are still active. + switch (awaiter.Kind) + { + case LockKind.Read: + read |= this.issuedReadLocks.Contains(awaiter); + break; + case LockKind.UpgradeableRead: + upgradeableRead |= this.issuedUpgradeableReadLocks.Contains(awaiter); + write |= this.IsStickyWriteUpgradedLock(awaiter); + break; + case LockKind.Write: + write |= this.issuedWriteLocks.Contains(awaiter); + break; + } + + if (read && upgradeableRead && write) + { + // We've seen it all. Walking the stack further would not provide anything more. + return; + } - this.joinableTaskContext = joinableTaskContext; - this.captureDiagnostics = captureDiagnostics; + awaiter = awaiter.NestingLock; + } + } } + } - /// - /// Flags that modify default lock behavior. - /// - [Flags] - public enum LockFlags + /// + /// Gets a value indicating whether all issued locks are merely the top-level lock or nesting locks of the specified lock. + /// + /// The most nested lock. + /// if all issued locks are the specified lock or nesting locks of it. + private bool AllHeldLocksAreByThisStack(Awaiter? awaiter) + { + Assumes.True(awaiter is null || !this.IsLockHeld(LockKind.Write, awaiter)); // this method doesn't yet handle sticky upgraded read locks (that appear in the write lock set). + lock (this.syncObject) { - /// - /// The default behavior applies. - /// - None = 0x0, + if (awaiter is object) + { + int locksMatched = 0; + while (awaiter is object) + { + if (this.GetActiveLockSet(awaiter.Kind).Contains(awaiter)) + { + locksMatched++; + } + + awaiter = awaiter.NestingLock; + } - /// - /// Causes an upgradeable reader to remain in an upgraded-write state once upgraded, - /// even after the nested write lock has been released. - /// - /// - /// This is useful when you have a batch of possible write operations to apply, which - /// may or may not actually apply in the end, but if any of them change anything, - /// all of their changes should be seen atomically (within a single write lock). - /// This approach is preferable to simply acquiring a write lock around the batch of - /// potential changes because it doesn't defeat concurrent readers until it knows there - /// is a change to actually make. - /// - StickyWrite = 0x1, + return locksMatched == this.issuedReadLocks.Count + this.issuedUpgradeableReadLocks.Count + this.issuedWriteLocks.Count; + } + else + { + return this.issuedReadLocks.Count == 0 && this.issuedUpgradeableReadLocks.Count == 0 && this.issuedWriteLocks.Count == 0; + } } + } - /// - /// An enumeration of the kinds of locks supported by this class. - /// - internal enum LockKind + /// + /// Gets a value indicating whether the specified lock is, or is a nested lock of, a given type. + /// + /// The kind of lock being queried for. + /// The (possibly nested) lock. + /// if the lock holder (also) holds the specified kind of lock. + private bool LockStackContains(LockKind kind, Awaiter? awaiter) + { + if (awaiter is object) { - /// - /// A lock that supports concurrently executing threads that hold this same lock type. - /// Holders of this lock may not obtain a lock without first - /// releasing all their locks. - /// - Read, + lock (this.syncObject) + { + HashSet? lockSet = this.GetActiveLockSet(kind); + while (awaiter is object) + { + // It's possible that this lock has been released (even mid-stack, due to our async nature), + // so only consider locks that are still active. + if (awaiter.Kind == kind && lockSet.Contains(awaiter)) + { + return true; + } - /// - /// A lock that may run concurrently with standard readers, but is exclusive of any other - /// upgradeable readers. Holders of this lock are allowed to obtain a write lock while - /// holding this lock to guarantee continuity of state between what they read and what they write. - /// - UpgradeableRead, + if (kind == LockKind.Write && this.IsStickyWriteUpgradedLock(awaiter)) + { + return true; + } - /// - /// A mutually exclusive lock. - /// - Write, + awaiter = awaiter.NestingLock; + } + } } - /// - /// Gets a value indicating whether any kind of lock is held by the caller and can - /// be immediately used given the caller's context. - /// - public bool IsAnyLockHeld - { - get { return this.IsReadLockHeld || this.IsUpgradeableReadLockHeld || this.IsWriteLockHeld; } - } + return false; + } - /// - /// Gets a value indicating whether any kind of lock is held by the caller without regard - /// to the lock compatibility of the caller's context. - /// - public bool IsAnyPassiveLockHeld + /// + /// Checks whether the specified lock is an upgradeable read lock, with a flag, + /// which has actually be upgraded. + /// + /// The lock to test. + /// if the test succeeds; otherwise. + private bool IsStickyWriteUpgradedLock(Awaiter awaiter) + { + if (awaiter.Kind == LockKind.UpgradeableRead && (awaiter.Options & LockFlags.StickyWrite) == LockFlags.StickyWrite) { - get { return this.IsPassiveReadLockHeld || this.IsPassiveUpgradeableReadLockHeld || this.IsPassiveWriteLockHeld; } + lock (this.syncObject) + { + return this.issuedWriteLocks.Contains(awaiter); + } } - /// - /// Gets a value indicating whether the caller holds a read lock. - /// - /// - /// This property returns false if any other lock type is held, unless - /// within that alternate lock type this lock is also nested. - /// - public bool IsReadLockHeld - { - get { return this.IsLockHeld(LockKind.Read); } - } + return false; + } - /// - /// Gets a value indicating whether a read lock is held by the caller without regard - /// to the lock compatibility of the caller's context. - /// - /// - /// This property returns false if any other lock type is held, unless - /// within that alternate lock type this lock is also nested. - /// - public bool IsPassiveReadLockHeld + /// + /// Checks whether the caller's held locks (or the specified lock stack) includes an active lock of the specified type. + /// Always when called on an STA thread. + /// + /// The type of lock to check for. + /// The most nested lock of the caller, or null to look up the caller's lock in the CallContext. + /// to throw an exception if the caller has an exclusive lock but not an associated SynchronizationContext. + /// to return true when a lock is held but unusable because of the context of the caller. + /// if the caller holds active locks of the given type; otherwise. + private bool IsLockHeld(LockKind kind, Awaiter? awaiter = null, bool checkSyncContextCompatibility = true, bool allowNonLockSupportingContext = false) + { + if (allowNonLockSupportingContext || this.IsLockSupportingContext(awaiter)) { - get { return this.IsLockHeld(LockKind.Read, checkSyncContextCompatibility: false, allowNonLockSupportingContext: true); } - } + lock (this.syncObject) + { + awaiter = awaiter ?? this.topAwaiter.Value; + if (checkSyncContextCompatibility) + { + this.CheckSynchronizationContextAppropriateForLock(awaiter); + } - /// - /// Gets a value indicating whether the caller holds an upgradeable read lock. - /// - /// - /// This property returns false if any other lock type is held, unless - /// within that alternate lock type this lock is also nested. - /// - public bool IsUpgradeableReadLockHeld - { - get { return this.IsLockHeld(LockKind.UpgradeableRead); } + return this.LockStackContains(kind, awaiter); + } } - /// - /// Gets a value indicating whether an upgradeable read lock is held by the caller without regard - /// to the lock compatibility of the caller's context. - /// - /// - /// This property returns false if any other lock type is held, unless - /// within that alternate lock type this lock is also nested. - /// - public bool IsPassiveUpgradeableReadLockHeld - { - get { return this.IsLockHeld(LockKind.UpgradeableRead, checkSyncContextCompatibility: false, allowNonLockSupportingContext: true); } - } + return false; + } - /// - /// Gets a value indicating whether the caller holds a write lock. - /// - /// - /// This property returns false if any other lock type is held, unless - /// within that alternate lock type this lock is also nested. - /// - public bool IsWriteLockHeld + /// + /// Checks whether a given lock is active. + /// Always when called on an STA thread. + /// + /// The lock to check. + /// if the return value will always be if called on an STA thread. + /// to throw an exception if the caller has an exclusive lock but not an associated SynchronizationContext. + /// if the lock is currently issued and the caller is not on an STA thread. + private bool IsLockActive(Awaiter awaiter, bool considerStaActive, bool checkSyncContextCompatibility = false) + { + Requires.NotNull(awaiter, nameof(awaiter)); + + if (considerStaActive || this.IsLockSupportingContext(awaiter)) { - get { return this.IsLockHeld(LockKind.Write); } + lock (this.syncObject) + { + bool activeLock = this.GetActiveLockSet(awaiter.Kind).Contains(awaiter); + if (checkSyncContextCompatibility && activeLock) + { + this.CheckSynchronizationContextAppropriateForLock(awaiter); + } + + return activeLock; + } } - /// - /// Gets a value indicating whether a write lock is held by the caller without regard - /// to the lock compatibility of the caller's context. - /// - /// - /// This property returns false if any other lock type is held, unless - /// within that alternate lock type this lock is also nested. - /// - public bool IsPassiveWriteLockHeld + return false; + } + + /// + /// Checks whether the specified awaiter's lock type has an associated SynchronizationContext if one is applicable. + /// + /// The awaiter whose lock should be considered. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] + private void CheckSynchronizationContextAppropriateForLock(Awaiter? awaiter) + { + ////bool syncContextRequired = this.LockStackContains(LockKind.UpgradeableRead, awaiter) || this.LockStackContains(LockKind.Write, awaiter); + ////if (syncContextRequired) { + //// if (!(SynchronizationContext.Current is NonConcurrentSynchronizationContext)) { + //// Assumes.Fail(); + //// } + ////} + } + + /// + /// Immediately issues a lock to the specified awaiter if it is available. + /// + /// The awaiter to issue a lock to. + /// + /// A value indicating whether this lock was previously queued. if this is a new just received request. + /// The value is used to determine whether to reject it if has already been called and this + /// is a new top-level request. + /// + /// + /// Normally, new reader locks are no longer issued when there is a pending writer lock to allow existing reader lock to complete. + /// However, that can lead deadlocks, when tasks with issued lock depending on tasks requiring new read locks to complete. + /// When it is true, new reader locks will be issued even when there is a pending writer lock. + /// + /// A value indicating whether the lock was issued. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] + private bool TryIssueLock(Awaiter awaiter, bool previouslyQueued, bool skipPendingWriteLockCheck = false) + { + bool issued = false; + bool isOrdinaryNestedLock = false; // ordinary nested lock is a nested lock always granted immediately. We don't need write ETW event to reduce noise in traces. + bool lockForkDetected = false; + + lock (this.syncObject) { - get { return this.IsLockHeld(LockKind.Write, checkSyncContextCompatibility: false, allowNonLockSupportingContext: true); } + if (this.completeInvoked && !previouslyQueued) + { + // If this is a new top-level lock request, reject it completely. + if (awaiter.NestingLock is null) + { + awaiter.SetFault(new InvalidOperationException(Strings.LockCompletionAlreadyRequested)); + return false; + } + } + + if (this.reenterConcurrencyPrepRunning is null) + { + if (this.issuedWriteLocks.Count == 0 && this.issuedUpgradeableReadLocks.Count == 0 && this.issuedReadLocks.Count == 0) + { + issued = true; + } + else + { + this.AggregateLockStackKinds(awaiter, out bool hasRead, out bool hasUpgradeableRead, out bool hasWrite); + switch (awaiter.Kind) + { + case LockKind.Read: + // Detect fork from upgradeable read context: the caller holds an active + // upgradeable read lock in its nesting chain but is executing on a thread + // that lacks the NonConcurrentSynchronizationContext, which typically means + // code used ConfigureAwait(false) and forked off the exclusive context. + // We skip this when hasWrite because the write fork path below already + // handles that case (with a hard failure), and we only report on the + // first request (!previouslyQueued) to avoid duplicate telemetry from + // PendAwaiter retries. + if (!previouslyQueued && hasUpgradeableRead && !hasWrite && + this.CanCurrentThreadHoldActiveLock && + !(SynchronizationContext.Current is NonConcurrentSynchronizationContext)) + { + lockForkDetected = true; + } + + if (this.issuedWriteLocks.Count == 0 && (skipPendingWriteLockCheck || this.waitingWriters.Count == 0)) + { + issued = true; + } + else if (hasWrite) + { + // We allow STA threads to not have the sync context applied because it never has it applied, + // and a write lock holder is allowed to transition to an STA tread. + // But if an MTA thread has the write lock but not the sync context, then they're likely + // an accidental execution fork that is exposing concurrency inappropriately. + if (this.CanCurrentThreadHoldActiveLock && !(SynchronizationContext.Current is NonConcurrentSynchronizationContext)) + { + Report.Fail("Dangerous request for read lock from fork of write lock."); + Verify.FailOperation(Strings.DangerousReadLockRequestFromWriteLockFork); + } + + issued = true; + isOrdinaryNestedLock = true; + } + else if (hasRead || hasUpgradeableRead) + { + issued = true; + isOrdinaryNestedLock = true; + } + + break; + case LockKind.UpgradeableRead: + if (hasUpgradeableRead || hasWrite) + { + issued = true; + isOrdinaryNestedLock = true; + } + else if (hasRead) + { + // We cannot issue an upgradeable read lock to folks who have (only) a read lock. + throw new InvalidOperationException(Strings.CannotUpgradeNonUpgradeableLock); + } +#pragma warning disable CA1508 // Avoid dead conditional code + else if (this.issuedUpgradeableReadLocks.Count == 0 && this.issuedWriteLocks.Count == 0) +#pragma warning restore CA1508 // Avoid dead conditional code + { + issued = true; + } + + break; + case LockKind.Write: + if (hasWrite) + { + issued = true; + isOrdinaryNestedLock = true; + } + else if (hasRead && !hasUpgradeableRead) + { + // We cannot issue a write lock when the caller already holds a read lock. + throw new InvalidOperationException(Strings.CannotUpgradeNonUpgradeableLock); + } + else if (this.AllHeldLocksAreByThisStack(awaiter.NestingLock)) + { + issued = true; + + Awaiter? stickyWriteAwaiter = this.FindRootUpgradeableReadWithStickyWrite(awaiter); + if (stickyWriteAwaiter is object) + { + // Add the upgradeable reader as a write lock as well. + this.issuedWriteLocks.Add(stickyWriteAwaiter); + } + } + + break; + default: + throw Assumes.NotReachable(); + } + } + } + + if (issued) + { + this.GetActiveLockSet(awaiter.Kind).Add(awaiter); + } } - /// - /// Gets a task whose completion signals that this lock will no longer issue locks. - /// - /// - /// This task only transitions to a complete state after a call to . - /// - public Task Completion + if (lockForkDetected) { - get { return this.completionSource.Task; } + this.OnLockForkDetected(); } - /// - /// Gets the object used to synchronize access to this instance's fields. - /// - protected object SyncObject + if (issued) { - get { return this.syncObject; } + if (!isOrdinaryNestedLock) + { + this.etw.Issued(awaiter); + } } - - /// - /// Gets the lock held by the caller's execution context. - /// - protected LockHandle AmbientLock + else { - get { return new LockHandle(this.GetFirstActiveSelfOrAncestor(this.topAwaiter.Value)); } + this.etw.WaitStart(awaiter); + + // If the lock is immediately available, we don't need to coordinate with other threads. + // But if it is NOT available, we'd have to wait potentially for other threads to do more work. + Debugger.NotifyOfCrossThreadDependency(); } - /// - /// Gets or sets a value indicating whether additional resources should be spent to collect - /// information that would be useful in diagnosing deadlocks, etc. - /// - protected bool CaptureDiagnostics + return issued; + } + + /// + /// Finds the upgradeable reader with flag that is nearest + /// to the top-level lock request held by the given lock holder. + /// + /// The awaiter to start the search down the stack from. + /// The least nested upgradeable reader lock with sticky write flag; or if none was found. + private Awaiter? FindRootUpgradeableReadWithStickyWrite(Awaiter? headAwaiter) + { + if (headAwaiter is null) { - get { return this.captureDiagnostics; } - set { this.captureDiagnostics = value; } + return null; } - /// - /// Gets a time delay to check whether pending writer lock and reader locks forms a deadlock. - /// - protected virtual TimeSpan DeadlockCheckTimeout => DefaultDeadlockCheckTimeout; - - /// - /// Gets a value indicating whether the current thread is allowed to - /// hold an active lock. - /// - /// - /// The default implementation of this property returns true - /// when the calling thread is NOT an STA thread. - /// This property may be overridden to return false - /// on threads that may compromise the integrity of the lock. - /// - protected virtual bool CanCurrentThreadHoldActiveLock + Awaiter? lowerMatch = this.FindRootUpgradeableReadWithStickyWrite(headAwaiter.NestingLock); + if (lowerMatch is object) { - get { return Thread.CurrentThread.GetApartmentState() != ApartmentState.STA; } + return lowerMatch; } - /// - /// Gets a value indicating whether the current SynchronizationContext is one that is not supported - /// by this lock. - /// - protected virtual bool IsUnsupportedSynchronizationContext + if (headAwaiter.Kind == LockKind.UpgradeableRead && (headAwaiter.Options & LockFlags.StickyWrite) == LockFlags.StickyWrite) { - get + lock (this.syncObject) { - SynchronizationContext? ctxt = SynchronizationContext.Current; - bool supported = ctxt is null || ctxt is NonConcurrentSynchronizationContext; - return !supported; + if (this.issuedUpgradeableReadLocks.Contains(headAwaiter)) + { + return headAwaiter; + } } } - /// - /// Obtains a read lock, asynchronously awaiting for the lock if it is not immediately available. - /// - /// - /// A token whose cancellation indicates lost interest in obtaining the lock. - /// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available, - /// a canceled token will cause the code that is waiting for the lock to resume with an . - /// - /// An awaitable object whose result is the lock releaser. - public Awaitable ReadLockAsync(CancellationToken cancellationToken = default(CancellationToken)) - { - return new Awaitable(this, LockKind.Read, LockFlags.None, cancellationToken); - } + return null; + } - /// - /// Obtains an upgradeable read lock, asynchronously awaiting for the lock if it is not immediately available. - /// - /// - /// A token whose cancellation indicates lost interest in obtaining the lock. - /// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available, - /// a canceled token will cause the code that is waiting for the lock to resume with an . - /// - /// An awaitable object whose result is the lock releaser. - public Awaitable UpgradeableReadLockAsync(CancellationToken cancellationToken = default(CancellationToken)) + /// + /// Gets the set of locks of a given kind. + /// + /// The kind of lock. + /// A set of locks. + private HashSet GetActiveLockSet(LockKind kind) + { + switch (kind) { - return new Awaitable(this, LockKind.UpgradeableRead, LockFlags.None, cancellationToken); + case LockKind.Read: + return this.issuedReadLocks; + case LockKind.UpgradeableRead: + return this.issuedUpgradeableReadLocks; + case LockKind.Write: + return this.issuedWriteLocks; + default: + throw Assumes.NotReachable(); } + } - /// - /// Obtains a read lock, asynchronously awaiting for the lock if it is not immediately available. - /// - /// Modifications to normal lock behavior. - /// - /// A token whose cancellation indicates lost interest in obtaining the lock. - /// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available, - /// a canceled token will cause the code that is waiting for the lock to resume with an . - /// - /// An awaitable object whose result is the lock releaser. - public Awaitable UpgradeableReadLockAsync(LockFlags options, CancellationToken cancellationToken = default(CancellationToken)) + /// + /// Gets the queue for a lock with a given type. + /// + /// The kind of lock. + /// A queue. + private Queue GetLockQueue(LockKind kind) + { + switch (kind) { - return new Awaitable(this, LockKind.UpgradeableRead, options, cancellationToken); + case LockKind.Read: + return this.waitingReaders; + case LockKind.UpgradeableRead: + return this.waitingUpgradeableReaders; + case LockKind.Write: + return this.waitingWriters; + default: + throw Assumes.NotReachable(); } + } - /// - /// Obtains a write lock, asynchronously awaiting for the lock if it is not immediately available. - /// - /// - /// A token whose cancellation indicates lost interest in obtaining the lock. - /// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available, - /// a canceled token will cause the code that is waiting for the lock to resume with an . - /// - /// An awaitable object whose result is the lock releaser. - public Awaitable WriteLockAsync(CancellationToken cancellationToken = default(CancellationToken)) + /// + /// Walks the nested lock stack until it finds an active one. + /// + /// The most nested lock to consider. May be null. + /// The first active lock encountered, or if none. + private Awaiter? GetFirstActiveSelfOrAncestor(Awaiter? awaiter) + { + while (awaiter is object) { - return new Awaitable(this, LockKind.Write, LockFlags.None, cancellationToken); - } + if (this.IsLockActive(awaiter, considerStaActive: true)) + { + break; + } - /// - /// Obtains a write lock, asynchronously awaiting for the lock if it is not immediately available. - /// - /// Modifications to normal lock behavior. - /// - /// A token whose cancellation indicates lost interest in obtaining the lock. - /// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available, - /// a canceled token will cause the code that is waiting for the lock to resume with an . - /// - /// An awaitable object whose result is the lock releaser. - public Awaitable WriteLockAsync(LockFlags options, CancellationToken cancellationToken = default(CancellationToken)) - { - return new Awaitable(this, LockKind.Write, options, cancellationToken); + awaiter = awaiter.NestingLock; } - /// - /// Prevents use or visibility of the caller's lock(s) until the returned value is disposed. - /// - /// The value to dispose to restore lock visibility. - /// - /// This can be used by a write lock holder that is about to fork execution to avoid - /// two threads simultaneously believing they hold the exclusive write lock. - /// The lock should be hidden just before kicking off the work and can be restored immediately - /// after kicking off the work. - /// - public Suppression HideLocks() - { - return new Suppression(this); - } + return awaiter; + } + + /// + /// Issues a lock to the specified awaiter and executes its continuation. + /// The awaiter should have already been dequeued. + /// + /// The awaiter to issue a lock to and execute. + private void IssueAndExecute(Awaiter awaiter) + { + EventsHelper.WaitStop(awaiter); + Assumes.True(this.TryIssueLock(awaiter, previouslyQueued: true, skipPendingWriteLockCheck: true)); + Assumes.True(this.ExecuteOrHandleCancellation(awaiter, stillInQueue: false)); + } - /// - /// Causes new top-level lock requests to be rejected and the task to transition - /// to a completed state after any issued locks have been released. - /// - public void Complete() + /// + /// Releases the lock held by the specified awaiter. + /// + /// The awaiter holding an active lock. + /// A value indicating whether the lock consumer ended up not executing any work. + /// + /// A task that should complete before the releasing thread accesses any resource protected by + /// a lock wrapping the lock being released. + /// The task will always be complete if is . + /// This method guarantees that the lock is effectively released from the caller, and the + /// can be safely recycled, before the synchronous portion of this method completes. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] + private Task ReleaseAsync(Awaiter awaiter, bool lockConsumerCanceled = false) + { + // This method does NOT use the async keyword in its signature to avoid CallContext changes that we make + // causing a fork/clone of the CallContext, which defeats our alloc-free uncontested lock story. + + // No one should have any locks to release (and be executing code) if we're in our intermediate state. + // When this test fails, it's because someone had an exclusive lock and allowed concurrently executing + // code to fork off and acquire a read (or upgradeable read?) lock, then outlive the parent write lock. + // This is an illegal pattern both because it means an exclusive lock is used concurrently (while the + // parent write lock is active) and when the write lock is released, it means that the child "read" + // lock suddenly became a "concurrent" lock, but we can't transition all the resources from exclusive + // access to concurrent access while someone is actually holding a lock (as such transition requires + // the lock class itself to have the exclusive lock to protect the resources going through the transition). + Awaiter? illegalConcurrentLock = this.reenterConcurrencyPrepRunning; // capture to local to preserve evidence in a concurrently reset field. + if (illegalConcurrentLock is object) { - lock (this.syncObject) + try { - this.completeInvoked = true; - this.CompleteIfAppropriate(); + Assumes.Fail(string.Format(CultureInfo.CurrentCulture, "Illegal concurrent use of exclusive lock. Exclusive lock: {0}, Nested lock that outlived parent: {1}", illegalConcurrentLock, awaiter)); } - } - - /// - /// Registers a callback to be invoked when the write lock held by the caller is - /// about to be ultimately released (outermost write lock). - /// - /// - /// The asynchronous delegate to invoke. - /// Access to the write lock is provided throughout the asynchronous invocation. - /// - /// - /// This supports some scenarios VC++ has where change event handlers need to inspect changes, - /// or follow up with other changes to respond to earlier changes, at the conclusion of the lock. - /// This method is safe to call from within a previously registered callback, in which case the - /// registered callback will run when previously registered callbacks have completed execution. - /// If the write lock is released to an upgradeable read lock, these callbacks are fired synchronously - /// with respect to the writer who is releasing the lock. Otherwise, the callbacks are invoked - /// asynchronously with respect to the releasing thread. - /// - public void OnBeforeWriteLockReleased(Func action) - { - Requires.NotNull(action, nameof(action)); - - lock (this.syncObject) + catch (Exception ex) { - if (!this.IsWriteLockHeld) - { - throw new InvalidOperationException(); - } - - this.beforeWriteReleasedCallbacks.Enqueue(action); + throw this.OnCriticalFailure(ex); } } - /// - public void Dispose() + if (!this.IsLockActive(awaiter, considerStaActive: true)) { - this.Dispose(true); - GC.SuppressFinalize(this); + return Task.CompletedTask; } - /// - /// Disposes managed and unmanaged resources held by this instance. - /// - /// true if was called; false if the object is being finalized. - protected virtual void Dispose(bool disposing) + Task? reenterConcurrentOutsideCode = null; + Task? synchronousCallbackExecution = null; + bool synchronousRequired = false; + Awaiter? remainingAwaiter = null; + Awaiter? topAwaiterAtStart = this.topAwaiter.Value; // do this outside the lock because it's fairly expensive and doesn't require the lock. + + lock (this.syncObject) { - if (disposing) - { - Timer? timerToDispose = null; + // In case this is a sticky write lock, it may also belong to the write locks issued collection. + bool upgradedStickyWrite = awaiter.Kind == LockKind.UpgradeableRead + && (awaiter.Options & LockFlags.StickyWrite) == LockFlags.StickyWrite + && this.issuedWriteLocks.Contains(awaiter); - lock (this.syncObject) + int writeLocksBefore = this.issuedWriteLocks.Count; + int upgradeableReadLocksBefore = this.issuedUpgradeableReadLocks.Count; + int writeLocksAfter = writeLocksBefore - ((awaiter.Kind == LockKind.Write || upgradedStickyWrite) ? 1 : 0); + int upgradeableReadLocksAfter = upgradeableReadLocksBefore - (awaiter.Kind == LockKind.UpgradeableRead ? 1 : 0); + bool finalExclusiveLockRelease = writeLocksBefore > 0 && writeLocksAfter == 0; + + Task callbackExecution = Task.CompletedTask; + if (!lockConsumerCanceled) + { + // Callbacks should be fired synchronously iff the last write lock is being released and read locks are already issued. + // This can occur when upgradeable read locks are held and upgraded, and then downgraded back to an upgradeable read. + callbackExecution = this.OnBeforeLockReleasedAsync(finalExclusiveLockRelease, new LockHandle(awaiter)) ?? Task.CompletedTask; + synchronousRequired = finalExclusiveLockRelease && upgradeableReadLocksAfter > 0; + if (synchronousRequired) { - timerToDispose = this.pendingWriterLockDeadlockCheckTimer; - this.pendingWriterLockDeadlockCheckTimer = null; + synchronousCallbackExecution = callbackExecution; } - - timerToDispose?.Dispose(); } - } - /// - /// Checks whether the aggregated flags from all locks in the lock stack satisfy the specified flag(s). - /// - /// The flag(s) that must be specified for a true result. - /// The head of the lock stack to consider. - /// true if all the specified flags are found somewhere in the lock stack; false otherwise. - protected bool LockStackContains(LockFlags flags, LockHandle handle) - { - LockFlags aggregateFlags = LockFlags.None; - Awaiter? awaiter = handle.Awaiter; - if (awaiter is object) + if (!lockConsumerCanceled) { - lock (this.syncObject) + if (writeLocksAfter == 0) { - while (awaiter is object) + bool fireWriteLockReleased = writeLocksBefore > 0; + bool fireUpgradeableReadLockReleased = upgradeableReadLocksBefore > 0 && upgradeableReadLocksAfter == 0; + if (fireWriteLockReleased || fireUpgradeableReadLockReleased) { - if (this.IsLockActive(awaiter, considerStaActive: true, checkSyncContextCompatibility: true)) + // The Task.Run is invoked from another method so that C# doesn't allocate the anonymous delegate + // it uses unless we actually are going to invoke it -- + if (fireWriteLockReleased) { - aggregateFlags |= awaiter.Options; - if ((aggregateFlags & flags) == flags) - { - return true; - } + reenterConcurrentOutsideCode = this.DowngradeLockAsync(awaiter, upgradedStickyWrite, fireUpgradeableReadLockReleased, callbackExecution); + } + else if (fireUpgradeableReadLockReleased) + { + this.OnUpgradeableReadLockReleased(); } - - awaiter = awaiter.NestingLock; } } } - return (aggregateFlags & flags) == flags; - } - - /// - /// Returns the aggregate of the lock flags for all nested locks. - /// - /// - /// This is not redundant with because that returns fast - /// once the presence of certain flag(s) is determined, whereas this will aggregate all flags, - /// some of which may be defined by derived types. - /// - protected LockFlags GetAggregateLockFlags() - { - LockFlags aggregateFlags = LockFlags.None; - Awaiter? awaiter = this.topAwaiter.Value; - if (awaiter is object) + if (reenterConcurrentOutsideCode is null) { - lock (this.syncObject) - { - while (awaiter is object) - { - if (this.IsLockActive(awaiter, considerStaActive: true, checkSyncContextCompatibility: true)) - { - aggregateFlags |= awaiter.Options; - } - - awaiter = awaiter.NestingLock; - } - } + this.OnReleaseReenterConcurrencyComplete(awaiter, upgradedStickyWrite, searchAllWaiters: false); } - return aggregateFlags; + remainingAwaiter = this.GetFirstActiveSelfOrAncestor(topAwaiterAtStart); } - /// - /// Fired when any lock is being released. - /// - /// true if the last write lock that the caller holds is being released; false otherwise. - /// The lock being released. - /// A task whose completion signals the conclusion of the asynchronous operation. - protected virtual Task OnBeforeLockReleasedAsync(bool exclusiveLockRelease, LockHandle releasingLock) + // Updating the topAwaiter requires touching the CallContext, which significantly increases the perf/GC hit + // for releasing locks. So we prefer to leave a released lock in the context and walk up the lock stack when + // necessary. But we will clean it up if it's the last lock released. + if (remainingAwaiter is null) { - // Raise the write release lock event if and only if this is the last write that is about to be released. - // Also check that issued read lock count is 0, because these callbacks themselves may acquire read locks - // on top of this write lock that hasn't quite gone away yet, and when they release their read lock, - // that shouldn't trigger a recursive call of the event. - if (exclusiveLockRelease) + // This assignment is outside the lock because it doesn't need the lock and it's a relatively expensive call + // that we needn't hold the lock for. + this.topAwaiter.Value = remainingAwaiter; + } + + if (synchronousRequired || true) + { // the "|| true" bit is to force us to always be synchronous when releasing locks until we can get all tests passing the other way. + if (reenterConcurrentOutsideCode is object && (synchronousCallbackExecution is object && !synchronousCallbackExecution.IsCompleted)) { - return this.OnBeforeExclusiveLockReleasedAsync(); + return Task.WhenAll(reenterConcurrentOutsideCode, synchronousCallbackExecution); } else { - return Task.CompletedTask; + return reenterConcurrentOutsideCode ?? synchronousCallbackExecution ?? Task.CompletedTask; } } + else + { + return Task.CompletedTask; + } + } - /// - /// Fired when the last write lock is about to be released. - /// - /// A task whose completion signals the conclusion of the asynchronous operation. - protected virtual Task OnBeforeExclusiveLockReleasedAsync() + /// + /// Schedules work on a background thread that will prepare protected resource(s) for concurrent access. + /// + private async Task DowngradeLockAsync(Awaiter awaiter, bool upgradedStickyWrite, bool fireUpgradeableReadLockReleased, Task beginAfterPrerequisite) + { + Requires.NotNull(awaiter, nameof(awaiter)); + Requires.NotNull(beginAfterPrerequisite, nameof(beginAfterPrerequisite)); + + Exception? prereqException = null; + try { - lock (this.SyncObject) - { - // While this method is called when the last write lock is about to be released, - // a derived type may override this method and have already taken an additional write lock, - // so only state our assumption in the non-derivation case. - Assumes.True(this.issuedWriteLocks.Count == 1 || !this.GetType().Equals(typeof(AsyncReaderWriterLock))); + await beginAfterPrerequisite.ConfigureAwait(SynchronizationContext.Current is NonConcurrentSynchronizationContext); + } + catch (Exception ex) + { + prereqException = ex; + } - if (this.beforeWriteReleasedCallbacks.Count > 0) - { - return this.InvokeBeforeWriteLockReleaseHandlersAsync(); - } - else + Task onExclusiveLockReleasedTask; + lock (this.syncObject) + { + // Check that no read locks are held. If they are, then that's a sign that + // within this write lock, someone took a read lock that is outliving the nesting + // write lock, which is a very dangerous situation. + if (this.issuedReadLocks.Count > 0) + { + if (this.HasAnyNestedLocks(awaiter)) { - return Task.CompletedTask; + try + { + throw new InvalidOperationException(Strings.WriteLockOutlived); + } + catch (InvalidOperationException ex) + { + this.OnCriticalFailure(ex); + } } } + + this.reenterConcurrencyPrepRunning = awaiter; + onExclusiveLockReleasedTask = this.OnExclusiveLockReleasedAsync(); } - /// - /// Get the task scheduler to execute the continuation when the lock is acquired. - /// AsyncReaderWriterLock uses a special to handle execusive locks, and will ignore task scheduler provided, so this is only used in a read lock scenario. - /// This method is called within the execution context to wait the read lock, so it can pick up based on the current execution context. - /// Note: the task scheduler is only used, when the lock is issued later. If the lock is issued immediately when returns true, it will be ignored. - /// - /// A task scheduler to schedule the continutation task when a lock is issued. - protected virtual TaskScheduler GetTaskSchedulerForReadLockRequest() + Exception? onExclusiveLockReleasedTaskException = null; + try + { + await onExclusiveLockReleasedTask.ConfigureAwait(false); + } + catch (Exception ex) { - return TaskScheduler.Default; + onExclusiveLockReleasedTaskException = ex; } - /// - /// Invoked after an exclusive lock is released but before anyone has a chance to enter the lock. - /// - /// - /// This method is called while holding a private lock in order to block future lock consumers till this method is finished. - /// - protected virtual Task OnExclusiveLockReleasedAsync() + if (fireUpgradeableReadLockReleased) { - return Task.CompletedTask; + // This will only fire when the outermost upgradeable read is not itself nested by a write lock, + // and that's by design. + this.OnUpgradeableReadLockReleased(); } - /// - /// Invoked when a top-level upgradeable read lock is released, leaving no remaining (write) lock. - /// - protected virtual void OnUpgradeableReadLockReleased() + lock (this.syncObject) { + this.reenterConcurrencyPrepRunning = null; + + // Skip updating the call context because we're in a forked execution context that won't + // ever impact the client code, and changing the CallContext now would cause the data to be cloned, + // allocating more memory wastefully. + this.OnReleaseReenterConcurrencyComplete(awaiter, upgradedStickyWrite, searchAllWaiters: true); } - /// - /// Invoked when the lock detects an internal error or illegal usage pattern that - /// indicates a serious flaw that should be immediately reported to the application - /// and/or bring down the process to avoid hangs or data corruption. - /// - /// The exception that captures the details of the failure. - /// An exception that may be returned by some implementations of tis method for he caller to rethrow. - protected virtual Exception OnCriticalFailure(Exception ex) + if (prereqException is object) { - Requires.NotNull(ex, nameof(ex)); + // rethrow the exception we experienced before, such that it doesn't wipe out its callstack. + System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(prereqException).Throw(); + } - Report.Fail(ex.Message); - Environment.FailFast(ex.ToString(), ex); - throw Assumes.NotReachable(); + if (onExclusiveLockReleasedTaskException is object) + { + // rethrow the exception we experienced before, such that it doesn't wipe out its callstack. + System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(onExclusiveLockReleasedTaskException).Throw(); } + } - /// - /// Invoked when the lock detects an internal error or illegal usage pattern that - /// indicates a serious flaw that should be immediately reported to the application - /// and/or bring down the process to avoid hangs or data corruption. - /// - /// The message to use for the exception. - /// An exception that may be returned by some implementations of tis method for he caller to rethrow. - protected Exception OnCriticalFailure(string message) + /// + /// Checks whether the specified lock has any active nested locks. + /// + private bool HasAnyNestedLocks(Awaiter lck) + { + Requires.NotNull(lck, nameof(lck)); + Assumes.True(Monitor.IsEntered(this.SyncObject)); + + return HasAnyNestedLocks(lck, this.issuedReadLocks) + || HasAnyNestedLocks(lck, this.issuedUpgradeableReadLocks) + || HasAnyNestedLocks(lck, this.issuedWriteLocks); + } + + /// + /// Called at the conclusion of releasing an exclusive lock to complete the transition. + /// + /// The awaiter being released. + /// A flag indicating whether the lock being released was an upgraded read lock with the sticky write flag set. + /// to scan the entire queue for pending lock requests that might qualify; used when qualifying locks were delayed for some reason besides lock contention. + private void OnReleaseReenterConcurrencyComplete(Awaiter awaiter, bool upgradedStickyWrite, bool searchAllWaiters) + { + Requires.NotNull(awaiter, nameof(awaiter)); + + lock (this.syncObject) { - try - { - throw Assumes.Fail(message); - } - catch (Exception ex) + Assumes.True(this.GetActiveLockSet(awaiter.Kind).Remove(awaiter)); + if (upgradedStickyWrite) { - throw this.OnCriticalFailure(ex); + Assumes.True(awaiter.Kind == LockKind.UpgradeableRead); + Assumes.True(this.issuedWriteLocks.Remove(awaiter)); } + + this.CompleteIfAppropriate(); + this.TryInvokeLockConsumer(searchAllWaiters); } + } - /// - /// Checks whether the specified lock has any active nested locks. - /// - private static bool HasAnyNestedLocks(Awaiter lck, HashSet lockCollection) + /// + /// Issues locks to one or more queued lock requests and executes their continuations + /// based on lock availability and policy-based prioritization (writer-friendly, etc.) + /// + /// to scan the entire queue for pending lock requests that might qualify; used when qualifying locks were delayed for some reason besides lock contention. + /// if any locks were issued; otherwise. + private bool TryInvokeLockConsumer(bool searchAllWaiters) + { + return this.TryInvokeOneWriterIfAppropriate(searchAllWaiters) + || this.TryInvokeOneUpgradeableReaderIfAppropriate(searchAllWaiters) + || this.TryInvokeAllReadersIfAppropriate(searchAllWaiters); + } + + /// + /// Invokes the final write lock release callbacks, if appropriate. + /// + /// A task representing the work of sequentially invoking the callbacks. + private async Task InvokeBeforeWriteLockReleaseHandlersAsync() + { + Assumes.True(Monitor.IsEntered(this.syncObject)); + Assumes.True(this.beforeWriteReleasedCallbacks.Count > 0); + + await using ((await new Awaitable(this, LockKind.Write, LockFlags.None, CancellationToken.None, checkSyncContextCompatibility: false)).ConfigureAwait(false)) { - Requires.NotNull(lck, nameof(lck)); - Requires.NotNull(lockCollection, nameof(lockCollection)); + await Task.Yield(); // ensure we've yielded to our caller, since the WriteLockAsync will not yield when on an MTA thread. - if (lockCollection.Count > 0) + // We sequentially loop over the callbacks rather than fire them concurrently because each callback + // gets visibility into the write lock, which of course provides exclusivity and concurrency would violate that. + // We also avoid executing the synchronous portions all in a row and awaiting them all + // because that too would violate an individual callback's sense of isolation in a write lock. + List? exceptions = null; + while (this.TryDequeueBeforeWriteReleasedCallback(out Func? callback)) { - foreach (Awaiter? nestedCandidate in lockCollection) + try + { + await callback().ConfigureAwait(true); + } + catch (Exception ex) { - if (nestedCandidate == lck) + if (exceptions is null) { - // This isn't nested -- it's the lock itself. - continue; + exceptions = new List(); } - for (Awaiter? a = nestedCandidate.NestingLock; a is object; a = a.NestingLock) - { - if (a == lck) - { - return true; - } - } + exceptions.Add(ex); } } - return false; - } - - private static void PendingWriterLockDeadlockWatchingCallback(object? state) - { - var readerWriterLock = (AsyncReaderWriterLock?)state; - Assumes.NotNull(readerWriterLock); - - readerWriterLock.TryInvokeAllDependentReadersIfAppropriate(); - - lock (readerWriterLock.syncObject) + if (exceptions is object) { - readerWriterLock.pendingWriterLockDeadlockCheckTimer?.Change((int)readerWriterLock.DeadlockCheckTimeout.TotalMilliseconds, -1); + throw new AggregateException(exceptions); } } + } - /// - /// Throws an exception if called on an STA thread. - /// - private void ThrowIfUnsupportedThreadOrSyncContext() + /// + /// Dequeues a single write lock release callback if available. + /// + /// Receives the callback to invoke, if any. + /// A value indicating whether a callback was available to invoke. + private bool TryDequeueBeforeWriteReleasedCallback([NotNullWhen(true)] out Func? callback) + { + lock (this.syncObject) { - if (!this.CanCurrentThreadHoldActiveLock) + if (this.beforeWriteReleasedCallbacks.Count > 0) { - Verify.FailOperation(Strings.STAThreadCallerNotAllowed); + callback = this.beforeWriteReleasedCallbacks.Dequeue(); + return true; } - - if (this.IsUnsupportedSynchronizationContext) + else { - Verify.FailOperation(Strings.AppliedSynchronizationContextNotAllowed); + callback = null; + return false; } } + } - /// - /// Gets a value indicating whether the caller's thread apartment model and SynchronizationContext - /// is compatible with a lock. - /// - private bool IsLockSupportingContext(Awaiter? awaiter = null) - { - if (!this.CanCurrentThreadHoldActiveLock || this.IsUnsupportedSynchronizationContext) - { - return false; - } + /// + /// Stores the specified lock in the CallContext dictionary. + /// + /// The awaiter that tracks the lock to grant to the caller. + private void ApplyLockToCallContext(Awaiter? topAwaiter) + { + Awaiter? awaiter = this.GetFirstActiveSelfOrAncestor(topAwaiter); + this.topAwaiter.Value = awaiter; + } - awaiter = awaiter ?? this.topAwaiter.Value; - if (this.IsLockHeld(LockKind.Write, awaiter, allowNonLockSupportingContext: true, checkSyncContextCompatibility: false) || - this.IsLockHeld(LockKind.UpgradeableRead, awaiter, allowNonLockSupportingContext: true, checkSyncContextCompatibility: false)) + /// + /// Issues locks to all queued reader lock requests if there are no issued write locks. + /// + /// to scan the entire queue for pending lock requests that might qualify; used when qualifying locks were delayed for some reason besides lock contention. + /// A value indicating whether any readers were issued locks. + private bool TryInvokeAllReadersIfAppropriate(bool searchAllWaiters) + { + bool invoked = false; + if (this.issuedWriteLocks.Count == 0 && this.waitingWriters.Count == 0) + { + while (this.waitingReaders.Count > 0) { - if (!(SynchronizationContext.Current is NonConcurrentSynchronizationContext)) - { - // Upgradeable read and write locks *must* have the NonConcurrentSynchronizationContext applied. - return false; - } + Awaiter? pendingReader = this.waitingReaders.Dequeue(); + Assumes.True(pendingReader.Kind == LockKind.Read); + this.IssueAndExecute(pendingReader); + invoked = true; } - - return true; } - - /// - /// Transitions the task to a completed state - /// if appropriate. - /// - private void CompleteIfAppropriate() + else if (searchAllWaiters) { - Assumes.True(Monitor.IsEntered(this.syncObject)); - - if (this.completeInvoked && - !this.completionSource.Task.IsCompleted && - this.reenterConcurrencyPrepRunning is null && - this.issuedReadLocks.Count == 0 && this.issuedUpgradeableReadLocks.Count == 0 && this.issuedWriteLocks.Count == 0 && - this.waitingReaders.Count == 0 && this.waitingUpgradeableReaders.Count == 0 && this.waitingWriters.Count == 0) + if (this.TryInvokeAnyWaitersInQueue(this.waitingReaders, breakOnFirstIssue: false)) { - // We must use another task to asynchronously transition this so we don't inadvertently execute continuations inline - // while we're holding a lock. - Task.Run(delegate { this.completionSource.TrySetResult(null); }); + return true; } } - /// - /// Detects which lock types the given lock holder has (including all nested locks). - /// - /// The most nested lock to be considered. - /// Receives a value indicating whether a read lock is held. - /// Receives a value indicating whether an upgradeable read lock is held. - /// Receives a value indicating whether a write lock is held. - private void AggregateLockStackKinds(Awaiter? awaiter, out bool read, out bool upgradeableRead, out bool write) - { - read = false; - upgradeableRead = false; - write = false; + return invoked; + } - if (awaiter is object) + private void TryInvokeAllDependentReadersIfAppropriate() + { + lock (this.syncObject) + { + if (this.issuedWriteLocks.Count == 0 && this.waitingWriters.Count > 0 && this.waitingReaders.Count > 0 && (this.issuedReadLocks.Count > 0 || this.issuedUpgradeableReadLocks.Count > 0)) { - lock (this.syncObject) + HashSet? dependentTasks = JoinableTaskDependencyGraph.GetDependentTasksFromCandidates( + this.issuedReadLocks.Concat(this.issuedUpgradeableReadLocks).Where(w => w.AmbientJoinableTask is not null).Select(w => w.AmbientJoinableTask!), + this.waitingReaders.Where(w => w.AmbientJoinableTask is not null).Select(w => w.AmbientJoinableTask!)); + + if (dependentTasks.Count > 0) { - while (awaiter is object) + int pendingCount = this.waitingReaders.Count; + while (pendingCount-- != 0) { - // It's possible that this lock has been released (even mid-stack, due to our async nature), - // so only consider locks that are still active. - switch (awaiter.Kind) + Awaiter pendingReader = this.waitingReaders.Dequeue(); + JoinableTask? readerContext = pendingReader.AmbientJoinableTask; + if (readerContext is not null && dependentTasks.Contains(readerContext)) { - case LockKind.Read: - read |= this.issuedReadLocks.Contains(awaiter); - break; - case LockKind.UpgradeableRead: - upgradeableRead |= this.issuedUpgradeableReadLocks.Contains(awaiter); - write |= this.IsStickyWriteUpgradedLock(awaiter); - break; - case LockKind.Write: - write |= this.issuedWriteLocks.Contains(awaiter); - break; + this.IssueAndExecute(pendingReader); } - - if (read && upgradeableRead && write) + else { - // We've seen it all. Walking the stack further would not provide anything more. - return; + this.waitingReaders.Enqueue(pendingReader); } - - awaiter = awaiter.NestingLock; } } } } + } - /// - /// Gets a value indicating whether all issued locks are merely the top-level lock or nesting locks of the specified lock. - /// - /// The most nested lock. - /// true if all issued locks are the specified lock or nesting locks of it. - private bool AllHeldLocksAreByThisStack(Awaiter? awaiter) + /// + /// Issues a lock to the next queued upgradeable reader, if no upgradeable read or write locks are currently issued. + /// + /// to scan the entire queue for pending lock requests that might qualify; used when qualifying locks were delayed for some reason besides lock contention. + /// A value indicating whether any upgradeable readers were issued locks. + private bool TryInvokeOneUpgradeableReaderIfAppropriate(bool searchAllWaiters) + { + if (this.issuedUpgradeableReadLocks.Count == 0 && this.issuedWriteLocks.Count == 0) { - Assumes.True(awaiter is null || !this.IsLockHeld(LockKind.Write, awaiter)); // this method doesn't yet handle sticky upgraded read locks (that appear in the write lock set). - lock (this.syncObject) + if (this.waitingUpgradeableReaders.Count > 0) { - if (awaiter is object) - { - int locksMatched = 0; - while (awaiter is object) - { - if (this.GetActiveLockSet(awaiter.Kind).Contains(awaiter)) - { - locksMatched++; - } - - awaiter = awaiter.NestingLock; - } - - return locksMatched == this.issuedReadLocks.Count + this.issuedUpgradeableReadLocks.Count + this.issuedWriteLocks.Count; - } - else - { - return this.issuedReadLocks.Count == 0 && this.issuedUpgradeableReadLocks.Count == 0 && this.issuedWriteLocks.Count == 0; - } + Awaiter? pendingUpgradeableReader = this.waitingUpgradeableReaders.Dequeue(); + Assumes.True(pendingUpgradeableReader.Kind == LockKind.UpgradeableRead); + this.IssueAndExecute(pendingUpgradeableReader); + return true; } } - - /// - /// Gets a value indicating whether the specified lock is, or is a nested lock of, a given type. - /// - /// The kind of lock being queried for. - /// The (possibly nested) lock. - /// true if the lock holder (also) holds the specified kind of lock. - private bool LockStackContains(LockKind kind, Awaiter? awaiter) + else if (searchAllWaiters) { - if (awaiter is object) + if (this.TryInvokeAnyWaitersInQueue(this.waitingUpgradeableReaders, breakOnFirstIssue: true)) { - lock (this.syncObject) - { - HashSet? lockSet = this.GetActiveLockSet(kind); - while (awaiter is object) - { - // It's possible that this lock has been released (even mid-stack, due to our async nature), - // so only consider locks that are still active. - if (awaiter.Kind == kind && lockSet.Contains(awaiter)) - { - return true; - } - - if (kind == LockKind.Write && this.IsStickyWriteUpgradedLock(awaiter)) - { - return true; - } - - awaiter = awaiter.NestingLock; - } - } + return true; } - - return false; } - /// - /// Checks whether the specified lock is an upgradeable read lock, with a flag, - /// which has actually be upgraded. - /// - /// The lock to test. - /// true if the test succeeds; false otherwise. - private bool IsStickyWriteUpgradedLock(Awaiter awaiter) + return false; + } + + /// + /// Issues a lock to the next queued writer, if no other locks are currently issued + /// or the last contending read lock was removed allowing a waiting upgradeable reader to upgrade. + /// + /// to scan the entire queue for pending lock requests that might qualify; used when qualifying locks were delayed for some reason besides lock contention. + /// A value indicating whether a writer was issued a lock. + private bool TryInvokeOneWriterIfAppropriate(bool searchAllWaiters) + { + if (this.issuedReadLocks.Count == 0 && this.issuedUpgradeableReadLocks.Count == 0 && this.issuedWriteLocks.Count == 0) { - if (awaiter.Kind == LockKind.UpgradeableRead && (awaiter.Options & LockFlags.StickyWrite) == LockFlags.StickyWrite) + if (this.waitingWriters.Count > 0) { - lock (this.syncObject) + Awaiter? pendingWriter = this.waitingWriters.Dequeue(); + if (this.waitingWriters.Count == 0) { - return this.issuedWriteLocks.Contains(awaiter); + this.StopPendingWriterLockDeadlockWatching(); } - } - return false; + Assumes.True(pendingWriter.Kind == LockKind.Write); + this.IssueAndExecute(pendingWriter); + return true; + } } - - /// - /// Checks whether the caller's held locks (or the specified lock stack) includes an active lock of the specified type. - /// Always false when called on an STA thread. - /// - /// The type of lock to check for. - /// The most nested lock of the caller, or null to look up the caller's lock in the CallContext. - /// true to throw an exception if the caller has an exclusive lock but not an associated SynchronizationContext. - /// true to return true when a lock is held but unusable because of the context of the caller. - /// true if the caller holds active locks of the given type; false otherwise. - private bool IsLockHeld(LockKind kind, Awaiter? awaiter = null, bool checkSyncContextCompatibility = true, bool allowNonLockSupportingContext = false) + else if (this.issuedUpgradeableReadLocks.Count > 0 || searchAllWaiters) { - if (allowNonLockSupportingContext || this.IsLockSupportingContext(awaiter)) + if (this.TryInvokeAnyWaitersInQueue(this.waitingWriters, breakOnFirstIssue: true)) { - lock (this.syncObject) - { - awaiter = awaiter ?? this.topAwaiter.Value; - if (checkSyncContextCompatibility) - { - this.CheckSynchronizationContextAppropriateForLock(awaiter); - } - - return this.LockStackContains(kind, awaiter); - } + return true; } - - return false; } - /// - /// Checks whether a given lock is active. - /// Always false when called on an STA thread. - /// - /// The lock to check. - /// if false the return value will always be false if called on an STA thread. - /// true to throw an exception if the caller has an exclusive lock but not an associated SynchronizationContext. - /// true if the lock is currently issued and the caller is not on an STA thread. - private bool IsLockActive(Awaiter awaiter, bool considerStaActive, bool checkSyncContextCompatibility = false) + return false; + } + + /// + /// Scans a lock awaiter queue for any that can be issued locks now. + /// + /// The queue to scan. + /// to break out immediately after issuing the first lock. + /// if any lock was issued; otherwise. + private bool TryInvokeAnyWaitersInQueue(Queue waiters, bool breakOnFirstIssue) + { + Requires.NotNull(waiters, nameof(waiters)); + + bool invoked = false; + bool invokedThisLoop; + do { - Requires.NotNull(awaiter, nameof(awaiter)); - - if (considerStaActive || this.IsLockSupportingContext(awaiter)) + invokedThisLoop = false; + foreach (Awaiter? lockWaiter in waiters) { - lock (this.syncObject) + if (this.TryIssueLock(lockWaiter, previouslyQueued: true)) { - bool activeLock = this.GetActiveLockSet(awaiter.Kind).Contains(awaiter); - if (checkSyncContextCompatibility && activeLock) + // Run the continuation asynchronously (since this is called in OnCompleted, which is an async pattern). + Assumes.True(this.ExecuteOrHandleCancellation(lockWaiter, stillInQueue: true)); + invoked = true; + invokedThisLoop = true; + if (breakOnFirstIssue) { - this.CheckSynchronizationContextAppropriateForLock(awaiter); + return true; } - return activeLock; + EventsHelper.WaitStop(lockWaiter); + + // At this point, the waiter was removed from the queue, so we can't keep + // enumerating the queue or we'll get an InvalidOperationException. + // Break out of the foreach, but the while loop will re-enter and we'll + // examine other possibilities. + break; } } - - return false; } + while (invokedThisLoop); // keep looping while we find matching locks. - /// - /// Checks whether the specified awaiter's lock type has an associated SynchronizationContext if one is applicable. - /// - /// The awaiter whose lock should be considered. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] - private void CheckSynchronizationContextAppropriateForLock(Awaiter? awaiter) - { - ////bool syncContextRequired = this.LockStackContains(LockKind.UpgradeableRead, awaiter) || this.LockStackContains(LockKind.Write, awaiter); - ////if (syncContextRequired) { - //// if (!(SynchronizationContext.Current is NonConcurrentSynchronizationContext)) { - //// Assumes.Fail(); - //// } - ////} - } + return invoked; + } - /// - /// Immediately issues a lock to the specified awaiter if it is available. - /// - /// The awaiter to issue a lock to. - /// - /// A value indicating whether this lock was previously queued. false if this is a new just received request. - /// The value is used to determine whether to reject it if has already been called and this - /// is a new top-level request. - /// - /// - /// Normally, new reader locks are no longer issued when there is a pending writer lock to allow existing reader lock to complete. - /// However, that can lead deadlocks, when tasks with issued lock depending on tasks requiring new read locks to complete. - /// When it is true, new reader locks will be issued even when there is a pending writer lock. - /// - /// A value indicating whether the lock was issued. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] - private bool TryIssueLock(Awaiter awaiter, bool previouslyQueued, bool skipPendingWriteLockCheck = false) + /// + /// Issues a lock to a lock waiter and execute its code if the lock is immediately available, otherwise + /// queues the lock request. + /// + /// The lock request. + private void PendAwaiter(Awaiter awaiter) + { + lock (this.syncObject) { - lock (this.syncObject) + if (this.TryIssueLock(awaiter, previouslyQueued: true)) { - if (this.completeInvoked && !previouslyQueued) - { - // If this is a new top-level lock request, reject it completely. - if (awaiter.NestingLock is null) - { - awaiter.SetFault(new InvalidOperationException(Strings.LockCompletionAlreadyRequested)); - return false; - } - } - - bool issued = false; - if (this.reenterConcurrencyPrepRunning is null) - { - if (this.issuedWriteLocks.Count == 0 && this.issuedUpgradeableReadLocks.Count == 0 && this.issuedReadLocks.Count == 0) - { - issued = true; - } - else - { - this.AggregateLockStackKinds(awaiter, out bool hasRead, out bool hasUpgradeableRead, out bool hasWrite); - switch (awaiter.Kind) - { - case LockKind.Read: - if (this.issuedWriteLocks.Count == 0 && (skipPendingWriteLockCheck || this.waitingWriters.Count == 0)) - { - issued = true; - } - else if (hasWrite) - { - // We allow STA threads to not have the sync context applied because it never has it applied, - // and a write lock holder is allowed to transition to an STA tread. - // But if an MTA thread has the write lock but not the sync context, then they're likely - // an accidental execution fork that is exposing concurrency inappropriately. - if (this.CanCurrentThreadHoldActiveLock && !(SynchronizationContext.Current is NonConcurrentSynchronizationContext)) - { -#if NETFRAMEWORK || NETCOREAPP // Assertion failures crash on .NET Core < 3.0 - Report.Fail("Dangerous request for read lock from fork of write lock."); -#endif - Verify.FailOperation(Strings.DangerousReadLockRequestFromWriteLockFork); - } - - issued = true; - } - else if (hasRead || hasUpgradeableRead) - { - issued = true; - } - - break; - case LockKind.UpgradeableRead: - if (hasUpgradeableRead || hasWrite) - { - issued = true; - } - else if (hasRead) - { - // We cannot issue an upgradeable read lock to folks who have (only) a read lock. - throw new InvalidOperationException(Strings.CannotUpgradeNonUpgradeableLock); - } -#pragma warning disable CA1508 // Avoid dead conditional code - else if (this.issuedUpgradeableReadLocks.Count == 0 && this.issuedWriteLocks.Count == 0) -#pragma warning restore CA1508 // Avoid dead conditional code - { - issued = true; - } - - break; - case LockKind.Write: - if (hasWrite) - { - issued = true; - } - else if (hasRead && !hasUpgradeableRead) - { - // We cannot issue a write lock when the caller already holds a read lock. - throw new InvalidOperationException(Strings.CannotUpgradeNonUpgradeableLock); - } - else if (this.AllHeldLocksAreByThisStack(awaiter.NestingLock)) - { - issued = true; - - Awaiter? stickyWriteAwaiter = this.FindRootUpgradeableReadWithStickyWrite(awaiter); - if (stickyWriteAwaiter is object) - { - // Add the upgradeable reader as a write lock as well. - this.issuedWriteLocks.Add(stickyWriteAwaiter); - } - } - - break; - default: - throw Assumes.NotReachable(); - } - } - } - - if (issued) - { - this.GetActiveLockSet(awaiter.Kind).Add(awaiter); - this.etw.Issued(awaiter); - } + // Run the continuation asynchronously (since this is called in OnCompleted, which is an async pattern). + Assumes.True(this.ExecuteOrHandleCancellation(awaiter, stillInQueue: false)); + } + else + { + Queue? queue = this.GetLockQueue(awaiter.Kind); + queue.Enqueue(awaiter); - if (!issued) + if (awaiter.Kind == LockKind.Write) { - this.etw.WaitStart(awaiter); - - // If the lock is immediately available, we don't need to coordinate with other threads. - // But if it is NOT available, we'd have to wait potentially for other threads to do more work. - Debugger.NotifyOfCrossThreadDependency(); + this.StartPendingWriterDeadlockTimerIfNecessary(); } - - return issued; } } + } - /// - /// Finds the upgradeable reader with flag that is nearest - /// to the top-level lock request held by the given lock holder. - /// - /// The awaiter to start the search down the stack from. - /// The least nested upgradeable reader lock with sticky write flag; or null if none was found. - private Awaiter? FindRootUpgradeableReadWithStickyWrite(Awaiter? headAwaiter) + private void StartPendingWriterDeadlockTimerIfNecessary() + { + if (this.joinableTaskContext is not null && + this.pendingWriterLockDeadlockCheckTimer is null && + this.waitingWriters.Count > 0 && + (this.issuedReadLocks.Count > 0 || this.issuedUpgradeableReadLocks.Count > 0)) { - if (headAwaiter is null) - { - return null; - } + this.pendingWriterLockDeadlockCheckTimer = new Timer(PendingWriterLockDeadlockWatchingCallback, this, (int)this.DeadlockCheckTimeout.TotalMilliseconds, -1); + } + } - Awaiter? lowerMatch = this.FindRootUpgradeableReadWithStickyWrite(headAwaiter.NestingLock); - if (lowerMatch is object) - { - return lowerMatch; - } + private void StopPendingWriterLockDeadlockWatching() + { + if (this.pendingWriterLockDeadlockCheckTimer is not null) + { + this.pendingWriterLockDeadlockCheckTimer.Dispose(); + this.pendingWriterLockDeadlockCheckTimer = null; + } + } - if (headAwaiter.Kind == LockKind.UpgradeableRead && (headAwaiter.Options & LockFlags.StickyWrite) == LockFlags.StickyWrite) + /// + /// Executes the lock receiver or releases the lock because the request for it was canceled before it was issued. + /// + /// The awaiter. + /// A value indicating whether the specified is expected to still be in the queue (and should be removed). + /// A value indicating whether a continuation delegate was actually invoked. + private bool ExecuteOrHandleCancellation(Awaiter awaiter, bool stillInQueue) + { + Requires.NotNull(awaiter, nameof(awaiter)); + + lock (this.SyncObject) + { + if (stillInQueue) { - lock (this.syncObject) + // The lock class can't deal well with cancelled lock requests remaining in its queue. + // Remove the awaiter, wherever in the queue it happens to be. + Queue? queue = this.GetLockQueue(awaiter.Kind); + if (!queue.RemoveMidQueue(awaiter)) { - if (this.issuedUpgradeableReadLocks.Contains(headAwaiter)) - { - return headAwaiter; - } + // This can happen when the lock request is cancelled, but during a race + // condition where the lock was just about to be issued anyway. + Assumes.True(awaiter.CancellationToken.IsCancellationRequested); + return false; } } - return null; + return awaiter.TryScheduleContinuationExecution(); } + } + /// + /// An awaitable that is returned from asynchronous lock requests. + /// + public readonly struct Awaitable + { /// - /// Gets the set of locks of a given kind. + /// The awaiter to return from the method. /// - /// The kind of lock. - /// A set of locks. - private HashSet GetActiveLockSet(LockKind kind) - { - switch (kind) - { - case LockKind.Read: - return this.issuedReadLocks; - case LockKind.UpgradeableRead: - return this.issuedUpgradeableReadLocks; - case LockKind.Write: - return this.issuedWriteLocks; - default: - throw Assumes.NotReachable(); - } - } + private readonly Awaiter? awaiter; /// - /// Gets the queue for a lock with a given type. + /// Initializes a new instance of the struct. /// - /// The kind of lock. - /// A queue. - private Queue GetLockQueue(LockKind kind) + /// The lock class that created this instance. + /// The type of lock being requested. + /// Any flags applied to the lock request. + /// The cancellation token. + /// to throw an exception if the caller has an exclusive lock but not an associated SynchronizationContext. + internal Awaitable(AsyncReaderWriterLock lck, LockKind kind, LockFlags options, CancellationToken cancellationToken, bool checkSyncContextCompatibility = true) { - switch (kind) + if (checkSyncContextCompatibility) + { + lck.CheckSynchronizationContextAppropriateForLock(lck.topAwaiter.Value); + } + + this.awaiter = new Awaiter(lck, kind, options, cancellationToken); + if (!cancellationToken.IsCancellationRequested) { - case LockKind.Read: - return this.waitingReaders; - case LockKind.UpgradeableRead: - return this.waitingUpgradeableReaders; - case LockKind.Write: - return this.waitingWriters; - default: - throw Assumes.NotReachable(); + lck.TryIssueLock(this.awaiter, previouslyQueued: false); } } /// - /// Walks the nested lock stack until it finds an active one. + /// Gets the awaiter value. /// - /// The most nested lock to consider. May be null. - /// The first active lock encountered, or null if none. - private Awaiter? GetFirstActiveSelfOrAncestor(Awaiter? awaiter) + public Awaiter GetAwaiter() { - while (awaiter is object) + if (this.awaiter is null) { - if (this.IsLockActive(awaiter, considerStaActive: true)) - { - break; - } - - awaiter = awaiter.NestingLock; + throw new InvalidOperationException(); } - return awaiter; + return this.awaiter; } + } + + /// + /// A value whose disposal releases a held lock. + /// + [DebuggerDisplay("{awaiter.kind}")] + public readonly struct Releaser : IDisposable, System.IAsyncDisposable + { + /// + /// The awaiter who manages the lifetime of a lock. + /// + private readonly Awaiter? awaiter; /// - /// Issues a lock to the specified awaiter and executes its continuation. - /// The awaiter should have already been dequeued. + /// Initializes a new instance of the struct. /// - /// The awaiter to issue a lock to and execute. - private void IssueAndExecute(Awaiter awaiter) + /// The awaiter. + internal Releaser(Awaiter awaiter) { - EventsHelper.WaitStop(awaiter); - Assumes.True(this.TryIssueLock(awaiter, previouslyQueued: true, skipPendingWriteLockCheck: true)); - Assumes.True(this.ExecuteOrHandleCancellation(awaiter, stillInQueue: false)); + this.awaiter = awaiter; } /// - /// Releases the lock held by the specified awaiter. + /// Releases the lock. /// - /// The awaiter holding an active lock. - /// A value indicating whether the lock consumer ended up not executing any work. - /// - /// A task that should complete before the releasing thread accesses any resource protected by - /// a lock wrapping the lock being released. - /// The task will always be complete if is true. - /// This method guarantees that the lock is effectively released from the caller, and the - /// can be safely recycled, before the synchronous portion of this method completes. - /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] - private Task ReleaseAsync(Awaiter awaiter, bool lockConsumerCanceled = false) - { - // This method does NOT use the async keyword in its signature to avoid CallContext changes that we make - // causing a fork/clone of the CallContext, which defeats our alloc-free uncontested lock story. - - // No one should have any locks to release (and be executing code) if we're in our intermediate state. - // When this test fails, it's because someone had an exclusive lock and allowed concurrently executing - // code to fork off and acquire a read (or upgradeable read?) lock, then outlive the parent write lock. - // This is an illegal pattern both because it means an exclusive lock is used concurrently (while the - // parent write lock is active) and when the write lock is released, it means that the child "read" - // lock suddenly became a "concurrent" lock, but we can't transition all the resources from exclusive - // access to concurrent access while someone is actually holding a lock (as such transition requires - // the lock class itself to have the exclusive lock to protect the resources going through the transition). - Awaiter? illegalConcurrentLock = this.reenterConcurrencyPrepRunning; // capture to local to preserve evidence in a concurrently reset field. - if (illegalConcurrentLock is object) - { - try - { - Assumes.Fail(string.Format(CultureInfo.CurrentCulture, "Illegal concurrent use of exclusive lock. Exclusive lock: {0}, Nested lock that outlived parent: {1}", illegalConcurrentLock, awaiter)); - } - catch (Exception ex) - { - throw this.OnCriticalFailure(ex); - } - } - - if (!this.IsLockActive(awaiter, considerStaActive: true)) - { - return Task.CompletedTask; - } - - Task? reenterConcurrentOutsideCode = null; - Task? synchronousCallbackExecution = null; - bool synchronousRequired = false; - Awaiter? remainingAwaiter = null; - Awaiter? topAwaiterAtStart = this.topAwaiter.Value; // do this outside the lock because it's fairly expensive and doesn't require the lock. - - lock (this.syncObject) + public void Dispose() + { + if (this.awaiter is object) { - // In case this is a sticky write lock, it may also belong to the write locks issued collection. - bool upgradedStickyWrite = awaiter.Kind == LockKind.UpgradeableRead - && (awaiter.Options & LockFlags.StickyWrite) == LockFlags.StickyWrite - && this.issuedWriteLocks.Contains(awaiter); - - int writeLocksBefore = this.issuedWriteLocks.Count; - int upgradeableReadLocksBefore = this.issuedUpgradeableReadLocks.Count; - int writeLocksAfter = writeLocksBefore - ((awaiter.Kind == LockKind.Write || upgradedStickyWrite) ? 1 : 0); - int upgradeableReadLocksAfter = upgradeableReadLocksBefore - (awaiter.Kind == LockKind.UpgradeableRead ? 1 : 0); - bool finalExclusiveLockRelease = writeLocksBefore > 0 && writeLocksAfter == 0; - - Task callbackExecution = Task.CompletedTask; - if (!lockConsumerCanceled) - { - // Callbacks should be fired synchronously iff the last write lock is being released and read locks are already issued. - // This can occur when upgradeable read locks are held and upgraded, and then downgraded back to an upgradeable read. - callbackExecution = this.OnBeforeLockReleasedAsync(finalExclusiveLockRelease, new LockHandle(awaiter)) ?? Task.CompletedTask; - synchronousRequired = finalExclusiveLockRelease && upgradeableReadLocksAfter > 0; - if (synchronousRequired) - { - synchronousCallbackExecution = callbackExecution; - } - } + var nonConcurrentSyncContext = SynchronizationContext.Current as NonConcurrentSynchronizationContext; - if (!lockConsumerCanceled) + // NOTE: when we have already called ReleaseAsync, and the lock has been released, + // we don't want to load the concurrent context and try to take it back immediately. If we do, it is possible + // that anther thread waiting for a write lock can take the concurrent context, so the current thread will be + // blocked and wait until it is done, and that makes it possible to run into the thread pool exhaustion trap. + if (!this.awaiter.IsReleased) { - if (writeLocksAfter == 0) + using (nonConcurrentSyncContext is object ? nonConcurrentSyncContext.LoanBackAnyHeldResource(this.awaiter.OwningLock) : default(NonConcurrentSynchronizationContext.LoanBack)) { - bool fireWriteLockReleased = writeLocksBefore > 0; - bool fireUpgradeableReadLockReleased = upgradeableReadLocksBefore > 0 && upgradeableReadLocksAfter == 0; - if (fireWriteLockReleased || fireUpgradeableReadLockReleased) + Task? releaseTask = this.awaiter.ReleaseAsync(); + using (NoMessagePumpSyncContext.Default.Apply()) { - // The Task.Run is invoked from another method so that C# doesn't allocate the anonymous delegate - // it uses unless we actually are going to invoke it -- - if (fireWriteLockReleased) + try { - reenterConcurrentOutsideCode = this.DowngradeLockAsync(awaiter, upgradedStickyWrite, fireUpgradeableReadLockReleased, callbackExecution); + while (!releaseTask.Wait(1000)) + { // this loop allows us to break into the debugger and step into managed code to analyze a hang. + } } - else if (fireUpgradeableReadLockReleased) + catch (AggregateException) { - this.OnUpgradeableReadLockReleased(); + // We want to throw the inner exception itself -- not the AggregateException. + releaseTask.GetAwaiter().GetResult(); } } } } - if (reenterConcurrentOutsideCode is null) - { - this.OnReleaseReenterConcurrencyComplete(awaiter, upgradedStickyWrite, searchAllWaiters: false); - } - - remainingAwaiter = this.GetFirstActiveSelfOrAncestor(topAwaiterAtStart); - } - - // Updating the topAwaiter requires touching the CallContext, which significantly increases the perf/GC hit - // for releasing locks. So we prefer to leave a released lock in the context and walk up the lock stack when - // necessary. But we will clean it up if it's the last lock released. - if (remainingAwaiter is null) - { - // This assignment is outside the lock because it doesn't need the lock and it's a relatively expensive call - // that we needn't hold the lock for. - this.topAwaiter.Value = remainingAwaiter; - } - - if (synchronousRequired || true) - { // the "|| true" bit is to force us to always be synchronous when releasing locks until we can get all tests passing the other way. - if (reenterConcurrentOutsideCode is object && (synchronousCallbackExecution is object && !synchronousCallbackExecution.IsCompleted)) - { - return Task.WhenAll(reenterConcurrentOutsideCode, synchronousCallbackExecution); - } - else + if (nonConcurrentSyncContext is object && !this.awaiter.OwningLock.AmbientLock.IsValid) { - return reenterConcurrentOutsideCode ?? synchronousCallbackExecution ?? Task.CompletedTask; + // The lock holder is taking the synchronous path to release the last UR/W lock held. + // Since they may go synchronously on their merry way for a while, forcibly release + // the sync context's semaphore that they otherwise would hold until their synchronous + // method returns. + nonConcurrentSyncContext.EarlyExitSynchronizationContext(); } } - else - { - return Task.CompletedTask; - } } /// - /// Schedules work on a background thread that will prepare protected resource(s) for concurrent access. + /// Releases the lock. /// - private async Task DowngradeLockAsync(Awaiter awaiter, bool upgradedStickyWrite, bool fireUpgradeableReadLockReleased, Task beginAfterPrerequisite) + public async ValueTask DisposeAsync() { - Requires.NotNull(awaiter, nameof(awaiter)); - Requires.NotNull(beginAfterPrerequisite, nameof(beginAfterPrerequisite)); - - Exception? prereqException = null; - try - { - await beginAfterPrerequisite.ConfigureAwait(SynchronizationContext.Current is NonConcurrentSynchronizationContext); - } - catch (Exception ex) - { - prereqException = ex; - } + await this.ReleaseAsync().ConfigureAwaitRunInline(); + this.Dispose(); + } - Task onExclusiveLockReleasedTask; - lock (this.syncObject) + /// + /// Asynchronously releases the lock. Dispose should still be called after this. + /// + /// + /// A task that should complete before the releasing thread accesses any resource protected by + /// a lock wrapping the lock being released. + /// + /// + /// Rather than calling this method explicitly, use the C# 8 "await using" syntax instead. + /// + public Task ReleaseAsync() + { + if (this.awaiter is object) { - // Check that no read locks are held. If they are, then that's a sign that - // within this write lock, someone took a read lock that is outliving the nesting - // write lock, which is a very dangerous situation. - if (this.issuedReadLocks.Count > 0) + var nonConcurrentSyncContext = SynchronizationContext.Current as NonConcurrentSynchronizationContext; + using (nonConcurrentSyncContext is object ? nonConcurrentSyncContext.LoanBackAnyHeldResource(this.awaiter.OwningLock) : default(NonConcurrentSynchronizationContext.LoanBack)) { - if (this.HasAnyNestedLocks(awaiter)) - { - try - { - throw new InvalidOperationException(Strings.WriteLockOutlived); - } - catch (InvalidOperationException ex) - { - this.OnCriticalFailure(ex); - } - } + return this.awaiter.ReleaseAsync(); } - - this.reenterConcurrencyPrepRunning = awaiter; - onExclusiveLockReleasedTask = this.OnExclusiveLockReleasedAsync(); - } - - Exception? onExclusiveLockReleasedTaskException = null; - try - { - await onExclusiveLockReleasedTask.ConfigureAwait(false); - } - catch (Exception ex) - { - onExclusiveLockReleasedTaskException = ex; } - if (fireUpgradeableReadLockReleased) - { - // This will only fire when the outermost upgradeable read is not itself nested by a write lock, - // and that's by design. - this.OnUpgradeableReadLockReleased(); - } + return Task.CompletedTask; + } + } - lock (this.syncObject) - { - this.reenterConcurrencyPrepRunning = null; + /// + /// A value whose disposal restores visibility of any locks held by the caller. + /// + public readonly struct Suppression : IDisposable + { + /// + /// The locking class. + /// + private readonly AsyncReaderWriterLock? lck; - // Skip updating the call context because we're in a forked execution context that won't - // ever impact the client code, and changing the CallContext now would cause the data to be cloned, - // allocating more memory wastefully. - this.OnReleaseReenterConcurrencyComplete(awaiter, upgradedStickyWrite, searchAllWaiters: true); - } + /// + /// The awaiter most recently acquired by the caller before hiding locks. + /// + private readonly Awaiter? awaiter; - if (prereqException is object) + /// + /// Initializes a new instance of the struct. + /// + /// The lock class. + internal Suppression(AsyncReaderWriterLock lck) + { + this.lck = lck; + this.awaiter = this.lck.topAwaiter.Value; + if (this.awaiter is object) { - // rethrow the exception we experienced before, such that it doesn't wipe out its callstack. - System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(prereqException).Throw(); + this.lck.topAwaiter.Value = null; } + } - if (onExclusiveLockReleasedTaskException is object) + /// + /// Restores visibility of hidden locks. + /// + public void Dispose() + { + if (this.lck is object) { - // rethrow the exception we experienced before, such that it doesn't wipe out its callstack. - System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(onExclusiveLockReleasedTaskException).Throw(); + this.lck.ApplyLockToCallContext(this.awaiter); } } + } + /// + /// A "public" representation of a specific lock. + /// + protected readonly struct LockHandle + { /// - /// Checks whether the specified lock has any active nested locks. + /// The awaiter this lock handle wraps. /// - private bool HasAnyNestedLocks(Awaiter lck) - { - Requires.NotNull(lck, nameof(lck)); - Assumes.True(Monitor.IsEntered(this.SyncObject)); - - return HasAnyNestedLocks(lck, this.issuedReadLocks) - || HasAnyNestedLocks(lck, this.issuedUpgradeableReadLocks) - || HasAnyNestedLocks(lck, this.issuedWriteLocks); - } + private readonly Awaiter? awaiter; /// - /// Called at the conclusion of releasing an exclusive lock to complete the transition. + /// Initializes a new instance of the struct. /// - /// The awaiter being released. - /// A flag indicating whether the lock being released was an upgraded read lock with the sticky write flag set. - /// true to scan the entire queue for pending lock requests that might qualify; used when qualifying locks were delayed for some reason besides lock contention. - private void OnReleaseReenterConcurrencyComplete(Awaiter awaiter, bool upgradedStickyWrite, bool searchAllWaiters) + internal LockHandle(Awaiter? awaiter) { - Requires.NotNull(awaiter, nameof(awaiter)); - - lock (this.syncObject) - { - Assumes.True(this.GetActiveLockSet(awaiter.Kind).Remove(awaiter)); - if (upgradedStickyWrite) - { - Assumes.True(awaiter.Kind == LockKind.UpgradeableRead); - Assumes.True(this.issuedWriteLocks.Remove(awaiter)); - } - - this.CompleteIfAppropriate(); - this.TryInvokeLockConsumer(searchAllWaiters); - } + this.awaiter = awaiter; } /// - /// Issues locks to one or more queued lock requests and executes their continuations - /// based on lock availability and policy-based prioritization (writer-friendly, etc.) + /// Gets a value indicating whether this handle is to a lock which was actually acquired. /// - /// true to scan the entire queue for pending lock requests that might qualify; used when qualifying locks were delayed for some reason besides lock contention. - /// true if any locks were issued; false otherwise. - private bool TryInvokeLockConsumer(bool searchAllWaiters) + public bool IsValid { - return this.TryInvokeOneWriterIfAppropriate(searchAllWaiters) - || this.TryInvokeOneUpgradeableReaderIfAppropriate(searchAllWaiters) - || this.TryInvokeAllReadersIfAppropriate(searchAllWaiters); + get { return this.awaiter is object; } } /// - /// Invokes the final write lock release callbacks, if appropriate. + /// Gets a value indicating whether this lock is still active. /// - /// A task representing the work of sequentially invoking the callbacks. - private async Task InvokeBeforeWriteLockReleaseHandlersAsync() + public bool IsActive { - Assumes.True(Monitor.IsEntered(this.syncObject)); - Assumes.True(this.beforeWriteReleasedCallbacks.Count > 0); - - await using ((await new Awaitable(this, LockKind.Write, LockFlags.None, CancellationToken.None, checkSyncContextCompatibility: false)).ConfigureAwait(false)) - { - await Task.Yield(); // ensure we've yielded to our caller, since the WriteLockAsync will not yield when on an MTA thread. - - // We sequentially loop over the callbacks rather than fire them concurrently because each callback - // gets visibility into the write lock, which of course provides exclusivity and concurrency would violate that. - // We also avoid executing the synchronous portions all in a row and awaiting them all - // because that too would violate an individual callback's sense of isolation in a write lock. - List? exceptions = null; - while (this.TryDequeueBeforeWriteReleasedCallback(out Func? callback)) - { - try - { - await callback().ConfigureAwait(true); - } - catch (Exception ex) - { - if (exceptions is null) - { - exceptions = new List(); - } - - exceptions.Add(ex); - } - } - - if (exceptions is object) - { - throw new AggregateException(exceptions); - } - } + get { return this.IsValid && this.awaiter!.OwningLock.IsLockActive(this.awaiter, considerStaActive: true); } } /// - /// Dequeues a single write lock release callback if available. - /// - /// Receives the callback to invoke, if any. - /// A value indicating whether a callback was available to invoke. - private bool TryDequeueBeforeWriteReleasedCallback([NotNullWhen(true)] out Func? callback) - { - lock (this.syncObject) - { - if (this.beforeWriteReleasedCallbacks.Count > 0) - { - callback = this.beforeWriteReleasedCallbacks.Dequeue(); - return true; - } - else - { - callback = null; - return false; - } - } + /// Gets a value indicating whether this lock represents a read lock. + /// + public bool IsReadLock + { + get { return this.IsValid ? this.awaiter!.Kind == LockKind.Read : false; } } /// - /// Stores the specified lock in the CallContext dictionary. + /// Gets a value indicating whether this lock represents an upgradeable read lock. /// - /// The awaiter that tracks the lock to grant to the caller. - private void ApplyLockToCallContext(Awaiter? topAwaiter) + public bool IsUpgradeableReadLock { - Awaiter? awaiter = this.GetFirstActiveSelfOrAncestor(topAwaiter); - this.topAwaiter.Value = awaiter; + get { return this.IsValid ? this.awaiter!.Kind == LockKind.UpgradeableRead : false; } } /// - /// Issues locks to all queued reader lock requests if there are no issued write locks. + /// Gets a value indicating whether this lock represents a write lock. /// - /// true to scan the entire queue for pending lock requests that might qualify; used when qualifying locks were delayed for some reason besides lock contention. - /// A value indicating whether any readers were issued locks. - private bool TryInvokeAllReadersIfAppropriate(bool searchAllWaiters) + public bool IsWriteLock { - bool invoked = false; - if (this.issuedWriteLocks.Count == 0 && this.waitingWriters.Count == 0) - { - while (this.waitingReaders.Count > 0) - { - Awaiter? pendingReader = this.waitingReaders.Dequeue(); - Assumes.True(pendingReader.Kind == LockKind.Read); - this.IssueAndExecute(pendingReader); - invoked = true; - } - } - else if (searchAllWaiters) - { - if (this.TryInvokeAnyWaitersInQueue(this.waitingReaders, breakOnFirstIssue: false)) - { - return true; - } - } - - return invoked; + get { return this.IsValid ? this.awaiter!.Kind == LockKind.Write : false; } } - private void TryInvokeAllDependentReadersIfAppropriate() + /// + /// Gets a value indicating whether this lock is an active read lock or is nested by one. + /// + public bool HasReadLock { - lock (this.syncObject) - { - if (this.issuedWriteLocks.Count == 0 && this.waitingWriters.Count > 0 && this.waitingReaders.Count > 0 && (this.issuedReadLocks.Count > 0 || this.issuedUpgradeableReadLocks.Count > 0)) - { - HashSet? dependentTasks = JoinableTaskDependencyGraph.GetDependentTasksFromCandidates( - this.issuedReadLocks.Concat(this.issuedUpgradeableReadLocks).Where(w => w.AmbientJoinableTask is not null).Select(w => w.AmbientJoinableTask!), - this.waitingReaders.Where(w => w.AmbientJoinableTask is not null).Select(w => w.AmbientJoinableTask!)); - - if (dependentTasks.Count > 0) - { - int pendingCount = this.waitingReaders.Count; - while (pendingCount-- != 0) - { - Awaiter pendingReader = this.waitingReaders.Dequeue(); - JoinableTask? readerContext = pendingReader.AmbientJoinableTask; - if (readerContext is not null && dependentTasks.Contains(readerContext)) - { - this.IssueAndExecute(pendingReader); - } - else - { - this.waitingReaders.Enqueue(pendingReader); - } - } - } - } - } + get { return this.IsValid ? this.awaiter!.OwningLock.IsLockHeld(LockKind.Read, this.awaiter, checkSyncContextCompatibility: false, allowNonLockSupportingContext: true) : false; } } /// - /// Issues a lock to the next queued upgradeable reader, if no upgradeable read or write locks are currently issued. + /// Gets a value indicating whether this lock is an active upgradeable read lock or is nested by one. /// - /// true to scan the entire queue for pending lock requests that might qualify; used when qualifying locks were delayed for some reason besides lock contention. - /// A value indicating whether any upgradeable readers were issued locks. - private bool TryInvokeOneUpgradeableReaderIfAppropriate(bool searchAllWaiters) + public bool HasUpgradeableReadLock { - if (this.issuedUpgradeableReadLocks.Count == 0 && this.issuedWriteLocks.Count == 0) - { - if (this.waitingUpgradeableReaders.Count > 0) - { - Awaiter? pendingUpgradeableReader = this.waitingUpgradeableReaders.Dequeue(); - Assumes.True(pendingUpgradeableReader.Kind == LockKind.UpgradeableRead); - this.IssueAndExecute(pendingUpgradeableReader); - return true; - } - } - else if (searchAllWaiters) - { - if (this.TryInvokeAnyWaitersInQueue(this.waitingUpgradeableReaders, breakOnFirstIssue: true)) - { - return true; - } - } - - return false; + get { return this.IsValid ? this.awaiter!.OwningLock.IsLockHeld(LockKind.UpgradeableRead, this.awaiter, checkSyncContextCompatibility: false, allowNonLockSupportingContext: true) : false; } } /// - /// Issues a lock to the next queued writer, if no other locks are currently issued - /// or the last contending read lock was removed allowing a waiting upgradeable reader to upgrade. + /// Gets a value indicating whether this lock is an active write lock or is nested by one. /// - /// true to scan the entire queue for pending lock requests that might qualify; used when qualifying locks were delayed for some reason besides lock contention. - /// A value indicating whether a writer was issued a lock. - private bool TryInvokeOneWriterIfAppropriate(bool searchAllWaiters) + public bool HasWriteLock { - if (this.issuedReadLocks.Count == 0 && this.issuedUpgradeableReadLocks.Count == 0 && this.issuedWriteLocks.Count == 0) - { - if (this.waitingWriters.Count > 0) - { - Awaiter? pendingWriter = this.waitingWriters.Dequeue(); - if (this.waitingWriters.Count == 0) - { - this.StopPendingWriterLockDeadlockWatching(); - } - - Assumes.True(pendingWriter.Kind == LockKind.Write); - this.IssueAndExecute(pendingWriter); - return true; - } - } - else if (this.issuedUpgradeableReadLocks.Count > 0 || searchAllWaiters) - { - if (this.TryInvokeAnyWaitersInQueue(this.waitingWriters, breakOnFirstIssue: true)) - { - return true; - } - } - - return false; + get { return this.IsValid ? this.awaiter!.OwningLock.IsLockHeld(LockKind.Write, this.awaiter, checkSyncContextCompatibility: false, allowNonLockSupportingContext: true) : false; } } /// - /// Scans a lock awaiter queue for any that can be issued locks now. + /// Gets the flags that were passed into this lock. /// - /// The queue to scan. - /// true to break out immediately after issuing the first lock. - /// true if any lock was issued; false otherwise. - private bool TryInvokeAnyWaitersInQueue(Queue waiters, bool breakOnFirstIssue) + public LockFlags Flags { - Requires.NotNull(waiters, nameof(waiters)); - - bool invoked = false; - bool invokedThisLoop; - do - { - invokedThisLoop = false; - foreach (Awaiter? lockWaiter in waiters) - { - if (this.TryIssueLock(lockWaiter, previouslyQueued: true)) - { - // Run the continuation asynchronously (since this is called in OnCompleted, which is an async pattern). - Assumes.True(this.ExecuteOrHandleCancellation(lockWaiter, stillInQueue: true)); - invoked = true; - invokedThisLoop = true; - if (breakOnFirstIssue) - { - return true; - } - - EventsHelper.WaitStop(lockWaiter); - - // At this point, the waiter was removed from the queue, so we can't keep - // enumerating the queue or we'll get an InvalidOperationException. - // Break out of the foreach, but the while loop will re-enter and we'll - // examine other possibilities. - break; - } - } - } - while (invokedThisLoop); // keep looping while we find matching locks. - - return invoked; + get { return this.IsValid ? this.awaiter!.Options : LockFlags.None; } } /// - /// Issues a lock to a lock waiter and execute its code if the lock is immediately available, otherwise - /// queues the lock request. + /// Gets or sets some object associated to this specific lock. /// - /// The lock request. - private void PendAwaiter(Awaiter awaiter) + public object? Data { - lock (this.syncObject) + get { - if (this.TryIssueLock(awaiter, previouslyQueued: true)) - { - // Run the continuation asynchronously (since this is called in OnCompleted, which is an async pattern). - Assumes.True(this.ExecuteOrHandleCancellation(awaiter, stillInQueue: false)); - } - else - { - Queue? queue = this.GetLockQueue(awaiter.Kind); - queue.Enqueue(awaiter); - - if (awaiter.Kind == LockKind.Write) - { - this.StartPendingWriterDeadlockTimerIfNecessary(); - } - } + return this.IsValid ? this.awaiter!.Data : null; } - } - private void StartPendingWriterDeadlockTimerIfNecessary() - { - if (this.joinableTaskContext is not null && - this.pendingWriterLockDeadlockCheckTimer is null && - this.waitingWriters.Count > 0 && - (this.issuedReadLocks.Count > 0 || this.issuedUpgradeableReadLocks.Count > 0)) + set { - this.pendingWriterLockDeadlockCheckTimer = new Timer(PendingWriterLockDeadlockWatchingCallback, this, (int)this.DeadlockCheckTimeout.TotalMilliseconds, -1); + Verify.Operation(this.IsValid, Strings.InvalidLock); + this.awaiter!.Data = value; } } - private void StopPendingWriterLockDeadlockWatching() + /// + /// Gets the lock within which this lock was acquired. + /// + public LockHandle NestingLock { - if (this.pendingWriterLockDeadlockCheckTimer is not null) - { - this.pendingWriterLockDeadlockCheckTimer.Dispose(); - this.pendingWriterLockDeadlockCheckTimer = null; - } + get { return this.IsValid ? new LockHandle(this.awaiter!.NestingLock) : default(LockHandle); } } /// - /// Executes the lock receiver or releases the lock because the request for it was canceled before it was issued. + /// Gets the wrapped awaiter. /// - /// The awaiter. - /// A value indicating whether the specified is expected to still be in the queue (and should be removed). - /// A value indicating whether a continuation delegate was actually invoked. - private bool ExecuteOrHandleCancellation(Awaiter awaiter, bool stillInQueue) + internal Awaiter? Awaiter { - Requires.NotNull(awaiter, nameof(awaiter)); + get { return this.awaiter; } + } + } - lock (this.SyncObject) - { - if (stillInQueue) - { - // The lock class can't deal well with cancelled lock requests remaining in its queue. - // Remove the awaiter, wherever in the queue it happens to be. - Queue? queue = this.GetLockQueue(awaiter.Kind); - if (!queue.RemoveMidQueue(awaiter)) - { - // This can happen when the lock request is cancelled, but during a race - // condition where the lock was just about to be issued anyway. - Assumes.True(awaiter.CancellationToken.IsCancellationRequested); - return false; - } - } + /// + /// Manages asynchronous access to a lock. + /// + [DebuggerDisplay("{kind}")] + public class Awaiter : ICriticalNotifyCompletion + { + /// + /// A singleton delegate for use in cancellation token registration to avoid memory allocations for delegates each time. + /// + private static readonly Action CancellationResponseAction = CancellationResponder; - return awaiter.TryScheduleContinuationExecution(); - } - } + /// + /// The instance of the lock class to which this awaiter is affiliated. + /// + private readonly AsyncReaderWriterLock lck; /// - /// An awaitable that is returned from asynchronous lock requests. + /// The type of lock requested. /// - public readonly struct Awaitable - { - /// - /// The awaiter to return from the method. - /// - private readonly Awaiter? awaiter; + private readonly LockKind kind; - /// - /// Initializes a new instance of the struct. - /// - /// The lock class that created this instance. - /// The type of lock being requested. - /// Any flags applied to the lock request. - /// The cancellation token. - /// true to throw an exception if the caller has an exclusive lock but not an associated SynchronizationContext. - internal Awaitable(AsyncReaderWriterLock lck, LockKind kind, LockFlags options, CancellationToken cancellationToken, bool checkSyncContextCompatibility = true) - { - if (checkSyncContextCompatibility) - { - lck.CheckSynchronizationContextAppropriateForLock(lck.topAwaiter.Value); - } + /// + /// The "parent" lock (i.e. the lock within which this lock is nested) if any. + /// + private readonly Awaiter? nestingLock; - this.awaiter = new Awaiter(lck, kind, options, cancellationToken); - if (!cancellationToken.IsCancellationRequested) - { - lck.TryIssueLock(this.awaiter, previouslyQueued: false); - } - } + /// + /// The cancellation token that would terminate waiting for a lock that is not yet available. + /// + private readonly CancellationToken cancellationToken; - /// - /// Gets the awaiter value. - /// - public Awaiter GetAwaiter() - { - if (this.awaiter is null) - { - throw new InvalidOperationException(); - } + /// + /// The flags applied to this lock. + /// + private readonly LockFlags options; - return this.awaiter; - } - } + /// + /// The stack trace of the caller originally requesting the lock. + /// + /// + /// This field is initialized only when is constructed with + /// the captureDiagnostics parameter set to . + /// + private readonly StackTrace? requestingStackTrace; /// - /// A value whose disposal releases a held lock. + /// The cancellation token event that should be disposed of to free memory when we no longer need to receive cancellation notifications. /// - [DebuggerDisplay("{awaiter.kind}")] - public readonly struct Releaser : IDisposable, System.IAsyncDisposable - { - /// - /// The awaiter who manages the lifetime of a lock. - /// - private readonly Awaiter? awaiter; + private CancellationTokenRegistration cancellationRegistration; - /// - /// Initializes a new instance of the struct. - /// - /// The awaiter. - internal Releaser(Awaiter awaiter) - { - this.awaiter = awaiter; - } + /// + /// Any exception to throw back to the lock requestor. + /// + private Exception? fault; - /// - /// Releases the lock. - /// - public void Dispose() - { - if (this.awaiter is object) - { - var nonConcurrentSyncContext = SynchronizationContext.Current as NonConcurrentSynchronizationContext; + /// + /// The continuation to execute when the lock is available. + /// + private Action? continuation; - // NOTE: when we have already called ReleaseAsync, and the lock has been released, - // we don't want to load the concurrent context and try to take it back immediately. If we do, it is possible - // that anther thread waiting for a write lock can take the concurrent context, so the current thread will be - // blocked and wait until it is done, and that makes it possible to run into the thread pool exhaustion trap. - if (!this.awaiter.IsReleased) - { - using (nonConcurrentSyncContext is object ? nonConcurrentSyncContext.LoanBackAnyHeldResource(this.awaiter.OwningLock) : default(NonConcurrentSynchronizationContext.LoanBack)) - { - Task? releaseTask = this.awaiter.ReleaseAsync(); - using (NoMessagePumpSyncContext.Default.Apply()) - { - try - { - while (!releaseTask.Wait(1000)) - { // this loop allows us to break into the debugger and step into managed code to analyze a hang. - } - } - catch (AggregateException) - { - // We want to throw the inner exception itself -- not the AggregateException. - releaseTask.GetAwaiter().GetResult(); - } - } - } - } + /// + /// The continuation we invoked to an issued lock. + /// + /// + /// We retain this value simply so that in hang reports we can identify the method we issued the lock to. + /// + private Action? continuationAfterLockIssued; - if (nonConcurrentSyncContext is object && !this.awaiter.OwningLock.AmbientLock.IsValid) - { - // The lock holder is taking the synchronous path to release the last UR/W lock held. - // Since they may go synchronously on their merry way for a while, forcibly release - // the sync context's semaphore that they otherwise would hold until their synchronous - // method returns. - nonConcurrentSyncContext.EarlyExitSynchronizationContext(); - } - } - } + /// + /// The TaskScheduler to invoke the continuation. + /// + private TaskScheduler? continuationTaskScheduler; - /// - /// Releases the lock. - /// - public async ValueTask DisposeAsync() - { - await this.ReleaseAsync().ConfigureAwaitRunInline(); - this.Dispose(); - } + /// + /// The task from a prior call to , if any. + /// + private Task? releaseAsyncTask; - /// - /// Asynchronously releases the lock. Dispose should still be called after this. - /// - /// - /// A task that should complete before the releasing thread accesses any resource protected by - /// a lock wrapping the lock being released. - /// - /// - /// Rather than calling this method explicitly, use the C# 8 "await using" syntax instead. - /// - public Task ReleaseAsync() - { - if (this.awaiter is object) - { - var nonConcurrentSyncContext = SynchronizationContext.Current as NonConcurrentSynchronizationContext; - using (nonConcurrentSyncContext is object ? nonConcurrentSyncContext.LoanBackAnyHeldResource(this.awaiter.OwningLock) : default(NonConcurrentSynchronizationContext.LoanBack)) - { - return this.awaiter.ReleaseAsync(); - } - } + /// + /// The synchronization context applied to folks who hold the lock. + /// + private SynchronizationContext? synchronizationContext; - return Task.CompletedTask; - } - } + /// + /// An arbitrary object that may be set by a derived type of the containing lock class. + /// + private object? data; /// - /// A value whose disposal restores visibility of any locks held by the caller. + /// Initializes a new instance of the class. /// - public readonly struct Suppression : IDisposable + /// The lock class creating this instance. + /// The type of lock being requested. + /// The flags to apply to the lock. + /// The cancellation token. + internal Awaiter(AsyncReaderWriterLock lck, LockKind kind, LockFlags options, CancellationToken cancellationToken) { - /// - /// The locking class. - /// - private readonly AsyncReaderWriterLock? lck; + Requires.NotNull(lck, nameof(lck)); - /// - /// The awaiter most recently acquired by the caller before hiding locks. - /// - private readonly Awaiter? awaiter; + this.lck = lck; + this.kind = kind; + this.options = options; + this.cancellationToken = cancellationToken; + this.nestingLock = lck.GetFirstActiveSelfOrAncestor(lck.topAwaiter.Value); + this.requestingStackTrace = lck.captureDiagnostics ? new StackTrace(2, true) : null; + this.AmbientJoinableTask = (this.nestingLock is null && this.kind != LockKind.Write) ? this.lck.joinableTaskContext?.AmbientTask : null; + } - /// - /// Initializes a new instance of the struct. - /// - /// The lock class. - internal Suppression(AsyncReaderWriterLock lck) + /// + /// Gets a value indicating whether the lock has been issued. + /// + public bool IsCompleted + { + get { - this.lck = lck; - this.awaiter = this.lck.topAwaiter.Value; - if (this.awaiter is object) + if (this.fault is object) { - this.lck.topAwaiter.Value = null; + return true; } - } - /// - /// Restores visibility of hidden locks. - /// - public void Dispose() - { - if (this.lck is object) + // If lock has already been issued, we have to switch to the right context, and ignore the CancellationToken. + if (this.lck.IsLockActive(this, considerStaActive: true)) { - this.lck.ApplyLockToCallContext(this.awaiter); + return this.lck.IsLockSupportingContext(this); } + + return this.cancellationToken.IsCancellationRequested; } } /// - /// A "public" representation of a specific lock. + /// Gets the lock instance that owns this awaiter. /// - protected readonly struct LockHandle + internal AsyncReaderWriterLock OwningLock { - /// - /// The awaiter this lock handle wraps. - /// - private readonly Awaiter? awaiter; + get { return this.lck; } + } - /// - /// Initializes a new instance of the struct. - /// - internal LockHandle(Awaiter? awaiter) - { - this.awaiter = awaiter; - } + /// + /// Gets the stack trace of the requestor of this lock. + /// + /// + /// Used for diagnostic purposes only. + /// + internal StackTrace? RequestingStackTrace + { + get { return this.requestingStackTrace; } + } - /// - /// Gets a value indicating whether this handle is to a lock which was actually acquired. - /// - public bool IsValid - { - get { return this.awaiter is object; } - } + /// + /// Gets the delegate to invoke (or that was invoked) when the lock is/was issued, if available. + /// FOR DIAGNOSTIC PURPOSES ONLY. + /// + internal Delegate? LockRequestingContinuation + { + get { return this.continuation ?? this.continuationAfterLockIssued; } + } - /// - /// Gets a value indicating whether this lock is still active. - /// - public bool IsActive - { - get { return this.IsValid && this.awaiter!.OwningLock.IsLockActive(this.awaiter, considerStaActive: true); } - } + /// + /// Gets the lock that the caller held before requesting this lock. + /// + internal Awaiter? NestingLock + { + get { return this.nestingLock; } + } - /// - /// Gets a value indicating whether this lock represents a read lock. - /// - public bool IsReadLock - { - get { return this.IsValid ? this.awaiter!.Kind == LockKind.Read : false; } - } + /// + /// Gets or sets an arbitrary object that may be set by a derived type of the containing lock class. + /// + internal object? Data + { + get { return this.data; } + set { this.data = value; } + } - /// - /// Gets a value indicating whether this lock represents an upgradeable read lock. - /// - public bool IsUpgradeableReadLock - { - get { return this.IsValid ? this.awaiter!.Kind == LockKind.UpgradeableRead : false; } - } + /// + /// Gets the cancellation token. + /// + internal CancellationToken CancellationToken + { + get { return this.cancellationToken; } + } - /// - /// Gets a value indicating whether this lock represents a write lock. - /// - public bool IsWriteLock - { - get { return this.IsValid ? this.awaiter!.Kind == LockKind.Write : false; } - } + /// + /// Gets the kind of lock being requested. + /// + internal LockKind Kind + { + get { return this.kind; } + } - /// - /// Gets a value indicating whether this lock is an active read lock or is nested by one. - /// - public bool HasReadLock - { - get { return this.IsValid ? this.awaiter!.OwningLock.IsLockHeld(LockKind.Read, this.awaiter, checkSyncContextCompatibility: false, allowNonLockSupportingContext: true) : false; } - } + /// + /// Gets the flags applied to this lock. + /// + internal LockFlags Options + { + get { return this.options; } + } - /// - /// Gets a value indicating whether this lock is an active upgradeable read lock or is nested by one. - /// - public bool HasUpgradeableReadLock + /// + /// Gets a value indicating whether the lock has already been released. + /// + internal bool IsReleased + { + get { - get { return this.IsValid ? this.awaiter!.OwningLock.IsLockHeld(LockKind.UpgradeableRead, this.awaiter, checkSyncContextCompatibility: false, allowNonLockSupportingContext: true) : false; } + return this.releaseAsyncTask is object && this.releaseAsyncTask.Status == TaskStatus.RanToCompletion; } + } - /// - /// Gets a value indicating whether this lock is an active write lock or is nested by one. - /// - public bool HasWriteLock - { - get { return this.IsValid ? this.awaiter!.OwningLock.IsLockHeld(LockKind.Write, this.awaiter, checkSyncContextCompatibility: false, allowNonLockSupportingContext: true) : false; } - } + /// + /// Gets the ambient JoinableTask when the lock is requested. This is used to resolve deadlock caused by issued read lock depending on new read lock requests blocked by pending write locks. + /// + internal JoinableTask? AmbientJoinableTask { get; } - /// - /// Gets the flags that were passed into this lock. - /// - public LockFlags Flags - { - get { return this.IsValid ? this.awaiter!.Options : LockFlags.None; } - } + /// + /// Gets a value indicating whether the lock is active. + /// + /// iff the lock has bee issued, has not yet been released, and the caller is on an MTA thread. + private bool LockIssued + { + get { return this.lck.IsLockActive(this, considerStaActive: false); } + } + + /// + /// Sets the delegate to execute when the lock is available. + /// + /// The delegate. + public void OnCompleted(Action continuation) => this.OnCompleted(continuation, flowExecutionContext: true); + + /// + /// Sets the delegate to execute when the lock is available + /// without flowing ExecutionContext. + /// + /// The delegate. + public void UnsafeOnCompleted(Action continuation) => this.OnCompleted(continuation, flowExecutionContext: false); - /// - /// Gets or sets some object associated to this specific lock. - /// - public object? Data + /// + /// Applies the issued lock to the caller and returns the value used to release the lock. + /// + /// The value to dispose of to release the lock. + public Releaser GetResult() + { + try { - get + this.cancellationRegistration.Dispose(); + + if (!this.LockIssued && this.continuation is null && !this.cancellationToken.IsCancellationRequested) { - return this.IsValid ? this.awaiter!.Data : null; + using (var synchronousBlock = new ManualResetEventSlim()) + { + this.OnCompleted(synchronousBlock.Set); + synchronousBlock.Wait(this.cancellationToken); + } } - set + if (this.fault is object) { - Verify.Operation(this.IsValid, Strings.InvalidLock); - this.awaiter!.Data = value; + throw this.fault; } - } - - /// - /// Gets the lock within which this lock was acquired. - /// - public LockHandle NestingLock - { - get { return this.IsValid ? new LockHandle(this.awaiter!.NestingLock) : default(LockHandle); } - } - - /// - /// Gets the wrapped awaiter. - /// - internal Awaiter? Awaiter - { - get { return this.awaiter; } - } - } - /// - /// Manages asynchronous access to a lock. - /// - [DebuggerDisplay("{kind}")] - public class Awaiter : ICriticalNotifyCompletion - { - /// - /// A singleton delegate for use in cancellation token registration to avoid memory allocations for delegates each time. - /// - private static readonly Action CancellationResponseAction = CancellationResponder; - - /// - /// The instance of the lock class to which this awaiter is affiliated. - /// - private AsyncReaderWriterLock lck; - - /// - /// The type of lock requested. - /// - private LockKind kind; - - /// - /// The "parent" lock (i.e. the lock within which this lock is nested) if any. - /// - private Awaiter? nestingLock; - - /// - /// The cancellation token that would terminate waiting for a lock that is not yet available. - /// - private CancellationToken cancellationToken; - - /// - /// The cancellation token event that should be disposed of to free memory when we no longer need to receive cancellation notifications. - /// - private CancellationTokenRegistration cancellationRegistration; - - /// - /// The flags applied to this lock. - /// - private LockFlags options; - - /// - /// Any exception to throw back to the lock requestor. - /// - private Exception? fault; - - /// - /// The continuation to execute when the lock is available. - /// - private Action? continuation; - - /// - /// The continuation we invoked to an issued lock. - /// - /// - /// We retain this value simply so that in hang reports we can identify the method we issued the lock to. - /// - private Action? continuationAfterLockIssued; - - /// - /// The TaskScheduler to invoke the continuation. - /// - private TaskScheduler? continuationTaskScheduler; - - /// - /// The task from a prior call to , if any. - /// - private Task? releaseAsyncTask; - - /// - /// The synchronization context applied to folks who hold the lock. - /// - private SynchronizationContext? synchronizationContext; - - /// - /// The stacktrace of the caller originally requesting the lock. - /// - /// - /// This field is initialized only when is constructed with - /// the captureDiagnostics parameter set to true. - /// - private StackTrace? requestingStackTrace; - - /// - /// An arbitrary object that may be set by a derived type of the containing lock class. - /// - private object? data; - - /// - /// Initializes a new instance of the class. - /// - /// The lock class creating this instance. - /// The type of lock being requested. - /// The flags to apply to the lock. - /// The cancellation token. - internal Awaiter(AsyncReaderWriterLock lck, LockKind kind, LockFlags options, CancellationToken cancellationToken) - { - Requires.NotNull(lck, nameof(lck)); - - this.lck = lck; - this.kind = kind; - this.options = options; - this.cancellationToken = cancellationToken; - this.nestingLock = lck.GetFirstActiveSelfOrAncestor(lck.topAwaiter.Value); - this.requestingStackTrace = lck.captureDiagnostics ? new StackTrace(2, true) : null; - this.AmbientJoinableTask = (this.nestingLock is null && this.kind != LockKind.Write) ? this.lck.joinableTaskContext?.AmbientTask : null; - } - - /// - /// Gets a value indicating whether the lock has been issued. - /// - public bool IsCompleted - { - get + if (this.LockIssued) { - if (this.fault is object) + this.lck.ThrowIfUnsupportedThreadOrSyncContext(); + if ((this.Kind & (LockKind.UpgradeableRead | LockKind.Write)) != 0) { - return true; + Assumes.True(SynchronizationContext.Current is NonConcurrentSynchronizationContext); } - // If lock has already been issued, we have to switch to the right context, and ignore the CancellationToken. - if (this.lck.IsLockActive(this, considerStaActive: true)) - { - return this.lck.IsLockSupportingContext(this); - } + this.lck.ApplyLockToCallContext(this); - return this.cancellationToken.IsCancellationRequested; + return new Releaser(this); + } + else if (this.cancellationToken.IsCancellationRequested) + { + // At this point, someone called GetResult who wasn't registered as a synchronous waiter, + // and before the lock was issued. + // If the cancellation token was signaled, we'll throw that because a canceled token is a + // legit reason to hit this path in the method. Otherwise it's an internal error. + throw new OperationCanceledException(); } - } - - /// - /// Gets the lock instance that owns this awaiter. - /// - internal AsyncReaderWriterLock OwningLock - { - get { return this.lck; } - } - /// - /// Gets the stack trace of the requestor of this lock. - /// - /// - /// Used for diagnostic purposes only. - /// - internal StackTrace? RequestingStackTrace - { - get { return this.requestingStackTrace; } + this.lck.ThrowIfUnsupportedThreadOrSyncContext(); + throw Assumes.NotReachable(); } - - /// - /// Gets the delegate to invoke (or that was invoked) when the lock is/was issued, if available. - /// FOR DIAGNOSTIC PURPOSES ONLY. - /// - internal Delegate? LockRequestingContinuation + catch (OperationCanceledException) { - get { return this.continuation ?? this.continuationAfterLockIssued; } + // Don't release at this point, or else it would recycle this instance prematurely + // (while it's still in the queue to receive a lock). + throw; } - - /// - /// Gets the lock that the caller held before requesting this lock. - /// - internal Awaiter? NestingLock + catch { - get { return this.nestingLock; } + this.ReleaseAsync(lockConsumerCanceled: true); + throw; } + } - /// - /// Gets or sets an arbitrary object that may be set by a derived type of the containing lock class. - /// - internal object? Data + /// + /// Releases the lock and recycles this instance. + /// + internal Task ReleaseAsync(bool lockConsumerCanceled = false) + { + if (this.releaseAsyncTask is null) { - get { return this.data; } - set { this.data = value; } + // This method does NOT use the async keyword in its signature to avoid CallContext changes that we make + // causing a fork/clone of the CallContext, which defeats our alloc-free uncontested lock story. + try + { + this.continuationAfterLockIssued = null; // clear field to defend against leaks if Awaiters live a long time. + this.releaseAsyncTask = this.lck.ReleaseAsync(this, lockConsumerCanceled); + } + catch (Exception ex) + { + // An exception here is *really* bad, because a project lock will get orphaned and + // a deadlock will soon result. + // Do what we can to save some evidence by capturing the exception in a faulted task. + // We don't need to rethrow the exception because we return the faulted task. + var tcs = new TaskCompletionSource(); + tcs.SetException(ex); + this.releaseAsyncTask = tcs.Task; + } } - /// - /// Gets the cancellation token. - /// - internal CancellationToken CancellationToken - { - get { return this.cancellationToken; } - } + return this.releaseAsyncTask; + } - /// - /// Gets the kind of lock being requested. - /// - internal LockKind Kind - { - get { return this.kind; } - } + /// + /// Executes the code that requires the lock. + /// + /// if the continuation was (asynchronously) invoked; if there was no continuation available to invoke. + internal bool TryScheduleContinuationExecution() + { + Action? continuation = Interlocked.Exchange(ref this.continuation, null); - /// - /// Gets the flags applied to this lock. - /// - internal LockFlags Options + if (continuation is object) { - get { return this.options; } - } + this.continuationAfterLockIssued = continuation; - /// - /// Gets a value indicating whether the lock has already been released. - /// - internal bool IsReleased - { - get + SynchronizationContext? synchronizationContext = this.GetEffectiveSynchronizationContext(); + if (this.continuationTaskScheduler is object && synchronizationContext == DefaultSynchronizationContext) { - return this.releaseAsyncTask is object && this.releaseAsyncTask.Status == TaskStatus.RanToCompletion; + Task.Factory.StartNew(continuation, CancellationToken.None, TaskCreationOptions.PreferFairness, this.continuationTaskScheduler); + } + else + { + synchronizationContext.Post(state => ((Action)state!)(), continuation); } - } - - /// - /// Gets the ambient JoinableTask when the lock is requested. This is used to resolve deadlock caused by issued read lock depending on new read lock requests blocked by pending write locks. - /// - internal JoinableTask? AmbientJoinableTask { get; } - /// - /// Gets a value indicating whether the lock is active. - /// - /// true iff the lock has bee issued, has not yet been released, and the caller is on an MTA thread. - private bool LockIssued + return true; + } + else { - get { return this.lck.IsLockActive(this, considerStaActive: false); } + return false; } + } - /// - /// Sets the delegate to execute when the lock is available. - /// - /// The delegate. - public void OnCompleted(Action continuation) => this.OnCompleted(continuation, flowExecutionContext: true); + /// + /// Specifies the exception to throw from . + /// + internal void SetFault(Exception ex) + { + this.fault = ex; + } - /// - /// Sets the delegate to execute when the lock is available - /// without flowing ExecutionContext. - /// - /// The delegate. - public void UnsafeOnCompleted(Action continuation) => this.OnCompleted(continuation, flowExecutionContext: false); + /// + /// Responds to lock request cancellation. + /// + /// The instance being canceled. + private static void CancellationResponder(object state) + { + var awaiter = (Awaiter)state; - /// - /// Applies the issued lock to the caller and returns the value used to release the lock. - /// - /// The value to dispose of to release the lock. - public Releaser GetResult() + // We're in a race with the lock suddenly becoming available. + // Our control in the race is asking the lock class to execute for us (within their private lock). + // unblock the awaiter immediately (which will then experience an OperationCanceledException). + if (awaiter.lck.ExecuteOrHandleCancellation(awaiter, stillInQueue: true)) { - try + // A pending write lock can block read locks, so we need issue them when the request is cancelled. + if (awaiter.Kind == LockKind.Write) { - this.cancellationRegistration.Dispose(); - - if (!this.LockIssued && this.continuation is null && !this.cancellationToken.IsCancellationRequested) - { - using (var synchronousBlock = new ManualResetEventSlim()) - { - this.OnCompleted(synchronousBlock.Set); - synchronousBlock.Wait(this.cancellationToken); - } - } - - if (this.fault is object) - { - throw this.fault; - } - - if (this.LockIssued) - { - this.lck.ThrowIfUnsupportedThreadOrSyncContext(); - if ((this.Kind & (LockKind.UpgradeableRead | LockKind.Write)) != 0) - { - Assumes.True(SynchronizationContext.Current is NonConcurrentSynchronizationContext); - } - - this.lck.ApplyLockToCallContext(this); - - return new Releaser(this); - } - else if (this.cancellationToken.IsCancellationRequested) + lock (awaiter.OwningLock.SyncObject) { - // At this point, someone called GetResult who wasn't registered as a synchronous waiter, - // and before the lock was issued. - // If the cancellation token was signaled, we'll throw that because a canceled token is a - // legit reason to hit this path in the method. Otherwise it's an internal error. - throw new OperationCanceledException(); + awaiter.OwningLock.TryInvokeLockConsumer(searchAllWaiters: false); } - - this.lck.ThrowIfUnsupportedThreadOrSyncContext(); - throw Assumes.NotReachable(); - } - catch (OperationCanceledException) - { - // Don't release at this point, or else it would recycle this instance prematurely - // (while it's still in the queue to receive a lock). - throw; - } - catch - { - this.ReleaseAsync(lockConsumerCanceled: true); - throw; } } - /// - /// Releases the lock and recycles this instance. - /// - internal Task ReleaseAsync(bool lockConsumerCanceled = false) + // Release memory of the registered handler, since we only need it to fire once. + awaiter.cancellationRegistration.Dispose(); + } + + /// + /// Get the correct SynchronizationContext to execute code executing within the lock. + /// Note: we need get the NonConcurrentSynchronizationContext from the nesting exclusive lock, because the child lock is essentially under the same context. + /// When we don't have a valid nesting lock, we will create a new NonConcurrentSynchronizationContext for an exclusive lock. For read lock, we don't put it within a NonConcurrentSynchronizationContext, + /// we set it to DefaultSynchronizationContext to mark we have computed it. The result is cached. + /// + private SynchronizationContext GetEffectiveSynchronizationContext() + { + if (this.synchronizationContext is null) { - if (this.releaseAsyncTask is null) + // Only read locks can be executed trivially. The locks that have some level of exclusivity (upgradeable read and write) + // must be executed via the NonConcurrentSynchronizationContext. + SynchronizationContext? synchronizationContext = null; + + Awaiter? awaiter = this.NestingLock; + while (awaiter is object) { - // This method does NOT use the async keyword in its signature to avoid CallContext changes that we make - // causing a fork/clone of the CallContext, which defeats our alloc-free uncontested lock story. - try + if (this.lck.IsLockActive(awaiter, considerStaActive: true)) { - this.continuationAfterLockIssued = null; // clear field to defend against leaks if Awaiters live a long time. - this.releaseAsyncTask = this.lck.ReleaseAsync(this, lockConsumerCanceled); - } - catch (Exception ex) - { - // An exception here is *really* bad, because a project lock will get orphaned and - // a deadlock will soon result. - // Do what we can to save some evidence by capturing the exception in a faulted task. - // We don't need to rethrow the exception because we return the faulted task. - var tcs = new TaskCompletionSource(); - tcs.SetException(ex); - this.releaseAsyncTask = tcs.Task; + synchronizationContext = awaiter.GetEffectiveSynchronizationContext(); + break; } - } - - return this.releaseAsyncTask; - } - /// - /// Executes the code that requires the lock. - /// - /// true if the continuation was (asynchronously) invoked; false if there was no continuation available to invoke. - internal bool TryScheduleContinuationExecution() - { - Action? continuation = Interlocked.Exchange(ref this.continuation, null); + awaiter = awaiter.NestingLock; + } - if (continuation is object) + if (synchronizationContext is null) { - this.continuationAfterLockIssued = continuation; - - SynchronizationContext? synchronizationContext = this.GetEffectiveSynchronizationContext(); - if (this.continuationTaskScheduler is object && synchronizationContext == DefaultSynchronizationContext) + if (this.kind == LockKind.Read) { - Task.Factory.StartNew(continuation, CancellationToken.None, TaskCreationOptions.PreferFairness, this.continuationTaskScheduler); + // We use DefaultSynchronizationContext to indicate that we have already computed the synchronizationContext once, and prevent repeating this logic second time. + synchronizationContext = DefaultSynchronizationContext; } else { - synchronizationContext.Post(state => ((Action)state!)(), continuation); + synchronizationContext = new NonConcurrentSynchronizationContext(); } - - return true; - } - else - { - return false; } + + Interlocked.CompareExchange(ref this.synchronizationContext, synchronizationContext, null); } - /// - /// Specifies the exception to throw from . - /// - internal void SetFault(Exception ex) + return this.synchronizationContext; + } + + /// + /// Sets the delegate to execute when the lock is available. + /// + /// The delegate. + /// A value indicating whether to flow ExecutionContext. + private void OnCompleted(Action continuation, bool flowExecutionContext) + { + if (this.LockIssued) { - this.fault = ex; + throw new InvalidOperationException(); } - /// - /// Responds to lock request cancellation. - /// - /// The instance being canceled. - private static void CancellationResponder(object state) + if (Interlocked.CompareExchange(ref this.continuation, continuation, null) is object) { - var awaiter = (Awaiter)state; - - // We're in a race with the lock suddenly becoming available. - // Our control in the race is asking the lock class to execute for us (within their private lock). - // unblock the awaiter immediately (which will then experience an OperationCanceledException). - if (awaiter.lck.ExecuteOrHandleCancellation(awaiter, stillInQueue: true)) - { - // A pending write lock can block read locks, so we need issue them when the request is cancelled. - if (awaiter.Kind == LockKind.Write) - { - lock (awaiter.OwningLock.SyncObject) - { - awaiter.OwningLock.TryInvokeLockConsumer(searchAllWaiters: false); - } - } - } - - // Release memory of the registered handler, since we only need it to fire once. - awaiter.cancellationRegistration.Dispose(); + throw new NotSupportedException(Strings.MultipleContinuationsNotSupported); } - /// - /// Get the correct SynchronizationContext to execute code executing within the lock. - /// Note: we need get the NonConcurrentSynchronizationContext from the nesting exclusive lock, because the child lock is essentially under the same context. - /// When we don't have a valid nesting lock, we will create a new NonConcurrentSynchronizationContext for an exclusive lock. For read lock, we don't put it within a NonConcurrentSynchronizationContext, - /// we set it to DefaultSynchronizationContext to mark we have computed it. The result is cached. - /// - private SynchronizationContext GetEffectiveSynchronizationContext() + bool restoreFlow = !flowExecutionContext && !ExecutionContext.IsFlowSuppressed(); + AsyncFlowControl flowControl = default; + if (restoreFlow) { - if (this.synchronizationContext is null) - { - // Only read locks can be executed trivially. The locks that have some level of exclusivity (upgradeable read and write) - // must be executed via the NonConcurrentSynchronizationContext. - SynchronizationContext? synchronizationContext = null; - - Awaiter? awaiter = this.NestingLock; - while (awaiter is object) - { - if (this.lck.IsLockActive(awaiter, considerStaActive: true)) - { - synchronizationContext = awaiter.GetEffectiveSynchronizationContext(); - break; - } - - awaiter = awaiter.NestingLock; - } - - if (synchronizationContext is null) - { - if (this.kind == LockKind.Read) - { - // We use DefaultSynchronizationContext to indicate that we have already computed the synchronizationContext once, and prevent repeating this logic second time. - synchronizationContext = DefaultSynchronizationContext; - } - else - { - synchronizationContext = new NonConcurrentSynchronizationContext(); - } - } - - Interlocked.CompareExchange(ref this.synchronizationContext, synchronizationContext, null); - } - - return this.synchronizationContext; + flowControl = ExecutionContext.SuppressFlow(); } - /// - /// Sets the delegate to execute when the lock is available. - /// - /// The delegate. - /// A value indicating whether to flow ExecutionContext. - private void OnCompleted(Action continuation, bool flowExecutionContext) + try { - if (this.LockIssued) + if (this.Kind == LockKind.Read) { - throw new InvalidOperationException(); + this.continuationTaskScheduler = this.OwningLock.GetTaskSchedulerForReadLockRequest(); } - if (Interlocked.CompareExchange(ref this.continuation, continuation, null) is object) + this.cancellationRegistration = this.cancellationToken.Register(CancellationResponseAction!, this, useSynchronizationContext: false); + this.lck.PendAwaiter(this); + + if (this.cancellationToken.IsCancellationRequested && this.cancellationRegistration == default(CancellationTokenRegistration)) { - throw new NotSupportedException(Strings.MultipleContinuationsNotSupported); + CancellationResponder(this); } - - bool restoreFlow = !flowExecutionContext && !ExecutionContext.IsFlowSuppressed(); - AsyncFlowControl flowControl = default; + } + finally + { if (restoreFlow) { - flowControl = ExecutionContext.SuppressFlow(); + flowControl.Dispose(); } + } + } + } - try - { - if (this.Kind == LockKind.Read) - { - this.continuationTaskScheduler = this.OwningLock.GetTaskSchedulerForReadLockRequest(); - } + internal sealed class NonConcurrentSynchronizationContext : SynchronizationContext, IDisposable + { + private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1); - this.cancellationRegistration = this.cancellationToken.Register(CancellationResponseAction!, this, useSynchronizationContext: false); - this.lck.PendAwaiter(this); + /// + /// The managed thread ID of the thread that has entered the semaphore. + /// + /// + /// No reason to lock around access to this field because it is only ever set to + /// or compared against the current thread, so the activity of other threads is irrelevant. + /// + private int? semaphoreHoldingManagedThreadId; - if (this.cancellationToken.IsCancellationRequested && this.cancellationRegistration == default(CancellationTokenRegistration)) - { - CancellationResponder(this); - } - } - finally - { - if (restoreFlow) - { - flowControl.Dispose(); - } - } + /// + /// Gets a value indicating whether the current thread holds the semaphore. + /// + private bool IsCurrentThreadHoldingSemaphore + { + get + { + // It is crucial that we capture the field in a local variable to guard against + // the scenario where this thread DOESN'T hold the semaphore but another has, and + // is in the process of clearing it, which would otherwise introduce a race condition + // where we check HasValue to be true, then try to call Value and it ends up throwing. + // Since int? is a value type, copying it to a local value guards against this race + // and we will simply return false in that case since our thread doesn't own it. + int? semaphoreHoldingManagedThreadId = this.semaphoreHoldingManagedThreadId; + return semaphoreHoldingManagedThreadId.HasValue + && semaphoreHoldingManagedThreadId.Value == Environment.CurrentManagedThreadId; } } - internal sealed class NonConcurrentSynchronizationContext : SynchronizationContext, IDisposable + public override void Send(SendOrPostCallback d, object? state) { - private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1); - - /// - /// The managed thread ID of the thread that has entered the semaphore. - /// - /// - /// No reason to lock around access to this field because it is only ever set to - /// or compared against the current thread, so the activity of other threads is irrelevant. - /// - private int? semaphoreHoldingManagedThreadId; + throw new NotSupportedException(); + } - /// - /// Gets a value indicating whether the current thread holds the semaphore. - /// - private bool IsCurrentThreadHoldingSemaphore - { - get - { - // It is crucial that we capture the field in a local variable to guard against - // the scenario where this thread DOESN'T hold the semaphore but another has, and - // is in the process of clearing it, which would otherwise introduce a race condition - // where we check HasValue to be true, then try to call Value and it ends up throwing. - // Since int? is a value type, copying it to a local value guards against this race - // and we will simply return false in that case since our thread doesn't own it. - int? semaphoreHoldingManagedThreadId = this.semaphoreHoldingManagedThreadId; - return semaphoreHoldingManagedThreadId.HasValue - && semaphoreHoldingManagedThreadId.Value == Environment.CurrentManagedThreadId; - } - } + public override void Post(SendOrPostCallback d, object? state) + { + Requires.NotNull(d, nameof(d)); - public override void Send(SendOrPostCallback d, object? state) + int? requestId = null; + if (ThreadingEventSource.Instance.IsEnabled()) { - throw new NotSupportedException(); + requestId = JoinableTaskFactory.SingleExecuteProtector.GetNextRequestId(); + ThreadingEventSource.Instance.PostExecutionStart(requestId.Value, false); } - public override void Post(SendOrPostCallback d, object? state) - { - Requires.NotNull(d, nameof(d)); - - if (ThreadingEventSource.Instance.IsEnabled()) + // Take special care to minimize allocations and overhead by avoiding implicit delegates and closures. + // The C# compiler caches this delegate in a static field because it never touches "this" + // nor any other local variables, which means the only allocations from this call + // are our Tuple and the ThreadPool's bare-minimum necessary to track the work. + ThreadPool.QueueUserWorkItem( + static s => { - ThreadingEventSource.Instance.PostExecutionStart(d.GetHashCode(), false); - } + var tuple = (Tuple)s!; + tuple.Item1.PostHelper(tuple.Item2, tuple.Item3, tuple.Item4); + }, + Tuple.Create(this, d, state, requestId)); + } - // Take special care to minimize allocations and overhead by avoiding implicit delegates and closures. - // The C# compiler caches this delegate in a static field because it never touches "this" - // nor any other local variables, which means the only allocations from this call - // are our Tuple and the ThreadPool's bare-minimum necessary to track the work. - ThreadPool.QueueUserWorkItem( - s => - { - var tuple = (Tuple)s!; - tuple.Item1.PostHelper(tuple.Item2, tuple.Item3); - }, - Tuple.Create(this, d, state)); - } + /// + public void Dispose() + { + this.semaphore.Dispose(); + } - /// - public void Dispose() + internal LoanBack LoanBackAnyHeldResource(AsyncReaderWriterLock asyncLock) + { + return (this.semaphore.CurrentCount == 0 && this.IsCurrentThreadHoldingSemaphore) + ? new LoanBack(this, asyncLock) + : default(LoanBack); + } + + internal void EarlyExitSynchronizationContext() + { + if (this.IsCurrentThreadHoldingSemaphore) { - this.semaphore.Dispose(); + this.semaphoreHoldingManagedThreadId = null; + this.semaphore.Release(); } - internal LoanBack LoanBackAnyHeldResource(AsyncReaderWriterLock asyncLock) + if (SynchronizationContext.Current == this) { - return (this.semaphore.CurrentCount == 0 && this.IsCurrentThreadHoldingSemaphore) - ? new LoanBack(this, asyncLock) - : default(LoanBack); + SynchronizationContext.SetSynchronizationContext(null); } + } - internal void EarlyExitSynchronizationContext() + /// + /// Executes the specified delegate. + /// + /// + /// We use async void instead of async Task because the caller will never + /// use the result, and this way the compiler doesn't have to create the Task object. + /// + private async void PostHelper(SendOrPostCallback d, object state, int? requestId) + { + bool delegateInvoked = false; + try { - if (this.IsCurrentThreadHoldingSemaphore) + await this.semaphore.WaitAsync().ConfigureAwait(false); + this.semaphoreHoldingManagedThreadId = Environment.CurrentManagedThreadId; + try { - this.semaphoreHoldingManagedThreadId = null; - this.semaphore.Release(); - } + SynchronizationContext.SetSynchronizationContext(this); + if (ThreadingEventSource.Instance.IsEnabled() && requestId.HasValue) + { + ThreadingEventSource.Instance.PostExecutionStop(requestId.Value); + } - if (SynchronizationContext.Current == this) + delegateInvoked = true; // set now, before the delegate might throw. + d(state); + } + catch (Exception ex) { - SynchronizationContext.SetSynchronizationContext(null); + // We just eat these up to avoid crashing the process by throwing on a threadpool thread. + Report.Fail("An unhandled exception was thrown from within a posted message. {0}", ex); + } + finally + { + // The semaphore *may* have been released already, so take care to not release it again. + if (this.IsCurrentThreadHoldingSemaphore) + { + this.semaphoreHoldingManagedThreadId = null; + this.semaphore.Release(); + } } } - - /// - /// Executes the specified delegate. - /// - /// - /// We use async void instead of async Task because the caller will never - /// use the result, and this way the compiler doesn't have to create the Task object. - /// - private async void PostHelper(SendOrPostCallback d, object state) + catch (ObjectDisposedException) { - bool delegateInvoked = false; - try + // It can happen that this SynchronizationContext was disposed of + // but someone who captured it is still trying to use it. + // In that case, we're not protecting anything any more and we're obliged + // to execute the delegate, so just execute it. + if (!delegateInvoked) { - await this.semaphore.WaitAsync().ConfigureAwait(false); - this.semaphoreHoldingManagedThreadId = Environment.CurrentManagedThreadId; + SynchronizationContext.SetSynchronizationContext(null); try { - SynchronizationContext.SetSynchronizationContext(this); - if (ThreadingEventSource.Instance.IsEnabled()) - { - ThreadingEventSource.Instance.PostExecutionStop(d.GetHashCode()); - } - delegateInvoked = true; // set now, before the delegate might throw. d(state); } @@ -2784,98 +2867,68 @@ private async void PostHelper(SendOrPostCallback d, object state) // We just eat these up to avoid crashing the process by throwing on a threadpool thread. Report.Fail("An unhandled exception was thrown from within a posted message. {0}", ex); } - finally - { - // The semaphore *may* have been released already, so take care to not release it again. - if (this.IsCurrentThreadHoldingSemaphore) - { - this.semaphoreHoldingManagedThreadId = null; - this.semaphore.Release(); - } - } - } - catch (ObjectDisposedException) - { - // It can happen that this SynchronizationContext was disposed of - // but someone who captured it is still trying to use it. - // In that case, we're not protecting anything any more and we're obliged - // to execute the delegate, so just execute it. - if (!delegateInvoked) - { - SynchronizationContext.SetSynchronizationContext(null); - try - { - delegateInvoked = true; // set now, before the delegate might throw. - d(state); - } - catch (Exception ex) - { - // We just eat these up to avoid crashing the process by throwing on a threadpool thread. - Report.Fail("An unhandled exception was thrown from within a posted message. {0}", ex); - } - } } } + } - internal readonly struct LoanBack : IDisposable - { - private readonly NonConcurrentSynchronizationContext syncContext; - private readonly AsyncReaderWriterLock asyncLock; + internal readonly struct LoanBack : IDisposable + { + private readonly NonConcurrentSynchronizationContext syncContext; + private readonly AsyncReaderWriterLock asyncLock; - internal LoanBack(NonConcurrentSynchronizationContext syncContext, AsyncReaderWriterLock asyncLock) - { - Requires.NotNull(syncContext, nameof(syncContext)); - Requires.NotNull(asyncLock, nameof(asyncLock)); - this.syncContext = syncContext; - this.asyncLock = asyncLock; - this.syncContext.semaphoreHoldingManagedThreadId = null; - this.syncContext.semaphore.Release(); - } + internal LoanBack(NonConcurrentSynchronizationContext syncContext, AsyncReaderWriterLock asyncLock) + { + Requires.NotNull(syncContext, nameof(syncContext)); + Requires.NotNull(asyncLock, nameof(asyncLock)); + this.syncContext = syncContext; + this.asyncLock = asyncLock; + this.syncContext.semaphoreHoldingManagedThreadId = null; + this.syncContext.semaphore.Release(); + } - public void Dispose() + public void Dispose() + { + if (this.syncContext is object) { - if (this.syncContext is object) - { - Assumes.False(Monitor.IsEntered(this.asyncLock.syncObject), "Should not wait on the Semaphore, when we hold the syncObject. This causes deadlocks"); - this.syncContext.semaphore.Wait(); - this.syncContext.semaphoreHoldingManagedThreadId = Environment.CurrentManagedThreadId; - } + Assumes.False(Monitor.IsEntered(this.asyncLock.syncObject), "Should not wait on the Semaphore, when we hold the syncObject. This causes deadlocks"); + this.syncContext.semaphore.Wait(); + this.syncContext.semaphoreHoldingManagedThreadId = Environment.CurrentManagedThreadId; } } } + } - internal class EventsHelper - { - private readonly AsyncReaderWriterLock lck; + internal class EventsHelper + { + private readonly AsyncReaderWriterLock lck; - internal EventsHelper(AsyncReaderWriterLock lck) - { - Requires.NotNull(lck, "lck"); - this.lck = lck; - } + internal EventsHelper(AsyncReaderWriterLock lck) + { + Requires.NotNull(lck, "lck"); + this.lck = lck; + } - internal static void WaitStop(Awaiter lckAwaiter) + internal static void WaitStop(Awaiter lckAwaiter) + { + if (ThreadingEventSource.Instance.IsEnabled()) { - if (ThreadingEventSource.Instance.IsEnabled()) - { - ThreadingEventSource.Instance.WaitReaderWriterLockStop(lckAwaiter.GetHashCode(), lckAwaiter.Kind); - } + ThreadingEventSource.Instance.WaitReaderWriterLockStop(lckAwaiter.GetHashCode(), lckAwaiter.Kind); } + } - internal void Issued(Awaiter lckAwaiter) + internal void Issued(Awaiter lckAwaiter) + { + if (ThreadingEventSource.Instance.IsEnabled()) { - if (ThreadingEventSource.Instance.IsEnabled()) - { - ThreadingEventSource.Instance.ReaderWriterLockIssued(lckAwaiter.GetHashCode(), lckAwaiter.Kind, this.lck.issuedUpgradeableReadLocks.Count, this.lck.issuedReadLocks.Count); - } + ThreadingEventSource.Instance.ReaderWriterLockIssued(lckAwaiter.GetHashCode(), lckAwaiter.Kind, this.lck.issuedUpgradeableReadLocks.Count, this.lck.issuedReadLocks.Count); } + } - internal void WaitStart(Awaiter lckAwaiter) + internal void WaitStart(Awaiter lckAwaiter) + { + if (ThreadingEventSource.Instance.IsEnabled()) { - if (ThreadingEventSource.Instance.IsEnabled()) - { - ThreadingEventSource.Instance.WaitReaderWriterLockStart(lckAwaiter.GetHashCode(), lckAwaiter.Kind, this.lck.issuedWriteLocks.Count, this.lck.issuedUpgradeableReadLocks.Count, this.lck.issuedReadLocks.Count); - } + ThreadingEventSource.Instance.WaitReaderWriterLockStart(lckAwaiter.GetHashCode(), lckAwaiter.Kind, this.lck.issuedWriteLocks.Count, this.lck.issuedUpgradeableReadLocks.Count, this.lck.issuedReadLocks.Count); } } } diff --git a/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterResourceLock`2.cs b/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterResourceLock`2.cs index c13b5123a..42cbbd914 100644 --- a/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterResourceLock`2.cs +++ b/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterResourceLock`2.cs @@ -1,904 +1,919 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// A non-blocking lock that allows concurrent access, exclusive access, or concurrent with upgradeability to exclusive access, +/// making special allowances for resources that must be prepared for concurrent or exclusive access. +/// +/// The type of the moniker that identifies a resource. +/// The type of resource issued for access by this lock. +public abstract class AsyncReaderWriterResourceLock : AsyncReaderWriterLock + where TResource : class { - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Diagnostics.CodeAnalysis; - using System.Linq; - using System.Runtime.CompilerServices; - using System.Threading; - using System.Threading.Tasks; + /// + /// A private nested class we use to isolate some of the behavior. + /// + private readonly Helper helper; + + /// + /// Initializes a new instance of the class. + /// + protected AsyncReaderWriterResourceLock() + { + this.helper = new Helper(this); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// to spend additional resources capturing diagnostic details that can be used + /// to analyze deadlocks or other issues. + protected AsyncReaderWriterResourceLock(bool captureDiagnostics) + : base(captureDiagnostics) + { + this.helper = new Helper(this); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// A JoinableTaskContext to help resolve dead locks caused by interdependency between top read lock tasks when there is a pending write lock blocking one of them. + /// + /// + /// to spend additional resources capturing diagnostic details that can be used + /// to analyze deadlocks or other issues. + protected AsyncReaderWriterResourceLock(JoinableTaskContext? joinableTaskContext, bool captureDiagnostics) + : base(joinableTaskContext, captureDiagnostics) + { + this.helper = new Helper(this); + } + + /// + /// Flags that modify default lock behavior. + /// + [Flags] + public new enum LockFlags + { + /// + /// The default behavior applies. + /// + None = 0x0, + + /// + /// Causes an upgradeable reader to remain in an upgraded-write state once upgraded, + /// even after the nested write lock has been released. + /// + /// + /// This is useful when you have a batch of possible write operations to apply, which + /// may or may not actually apply in the end, but if any of them change anything, + /// all of their changes should be seen atomically (within a single write lock). + /// This approach is preferable to simply acquiring a write lock around the batch of + /// potential changes because it doesn't defeat concurrent readers until it knows there + /// is a change to actually make. + /// + StickyWrite = 0x1, + + /// + /// Skips a step to make sure that the resource is initially prepared when retrieved using GetResourceAsync. + /// + /// + /// This flag is dormant for non-write locks. But if present on an upgradeable read lock, + /// this flag will activate for a nested write lock. + /// + SkipInitialPreparation = 0x1000, + } + + /// + /// Obtains a read lock, asynchronously awaiting for the lock if it is not immediately available. + /// + /// + /// A token whose cancellation indicates lost interest in obtaining the lock. + /// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available, + /// a canceled token will cause the code that is waiting for the lock to resume with an . + /// + /// An awaitable object whose result is the lock releaser. + public new ResourceAwaitable ReadLockAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + return new ResourceAwaitable(base.ReadLockAsync(cancellationToken), this.helper); + } + + /// + /// Obtains a read lock, asynchronously awaiting for the lock if it is not immediately available. + /// + /// Modifications to normal lock behavior. + /// + /// A token whose cancellation indicates lost interest in obtaining the lock. + /// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available, + /// a canceled token will cause the code that is waiting for the lock to resume with an . + /// + /// An awaitable object whose result is the lock releaser. + public ResourceAwaitable UpgradeableReadLockAsync(LockFlags options, CancellationToken cancellationToken = default(CancellationToken)) + { + return new ResourceAwaitable(this.UpgradeableReadLockAsync((AsyncReaderWriterLock.LockFlags)options, cancellationToken), this.helper); + } + + /// + /// Obtains an upgradeable read lock, asynchronously awaiting for the lock if it is not immediately available. + /// + /// + /// A token whose cancellation indicates lost interest in obtaining the lock. + /// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available, + /// a canceled token will cause the code that is waiting for the lock to resume with an . + /// + /// An awaitable object whose result is the lock releaser. + public new ResourceAwaitable UpgradeableReadLockAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + return new ResourceAwaitable(base.UpgradeableReadLockAsync(cancellationToken), this.helper); + } + + /// + /// Obtains a write lock, asynchronously awaiting for the lock if it is not immediately available. + /// + /// + /// A token whose cancellation indicates lost interest in obtaining the lock. + /// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available, + /// a canceled token will cause the code that is waiting for the lock to resume with an . + /// + /// An awaitable object whose result is the lock releaser. + public new ResourceAwaitable WriteLockAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + return new ResourceAwaitable(base.WriteLockAsync(cancellationToken), this.helper); + } + + /// + /// Obtains a write lock, asynchronously awaiting for the lock if it is not immediately available. + /// + /// Modifications to normal lock behavior. + /// + /// A token whose cancellation indicates lost interest in obtaining the lock. + /// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available, + /// a canceled token will cause the code that is waiting for the lock to resume with an . + /// + /// An awaitable object whose result is the lock releaser. + public ResourceAwaitable WriteLockAsync(LockFlags options, CancellationToken cancellationToken = default(CancellationToken)) + { + return new ResourceAwaitable(this.WriteLockAsync((AsyncReaderWriterLock.LockFlags)options, cancellationToken), this.helper); + } + + /// + /// Retrieves the resource with the specified moniker. + /// + /// The identifier for the desired resource. + /// A token whose cancellation indicates lost interest in obtaining the resource. + /// A task whose result is the desired resource. + protected abstract Task GetResourceAsync(TMoniker resourceMoniker, CancellationToken cancellationToken); + + /// + /// Marks a resource as having been retrieved under a lock. + /// + protected void SetResourceAsAccessed(TResource resource) + { + this.helper.SetResourceAsAccessed(resource); + } + + /// + /// Marks any loaded resources as having been retrieved under a lock if they + /// satisfy some predicate. + /// + /// A function that returns if the provided resource should be considered retrieved. + /// The state object to pass as a second parameter to . + /// if the delegate returned on any of the invocations. + protected bool SetResourceAsAccessed(Func resourceCheck, object? state) + { + return this.helper.SetResourceAsAccessed(resourceCheck, state); + } + + /// + /// Sets all the resources to be considered in an unknown state. + /// + protected void SetAllResourcesToUnknownState() + { + Verify.Operation(this.IsWriteLockHeld, Strings.InvalidLock); + this.helper.SetAllResourcesToUnknownState(); + } + + /// + /// Returns the aggregate of the lock flags for all nested locks. + /// + protected new LockFlags GetAggregateLockFlags() + { + return (LockFlags)base.GetAggregateLockFlags(); + } + + /// + /// Gets a task scheduler to prepare a resource for concurrent access. + /// + /// The resource to prepare. + /// A . + protected virtual TaskScheduler GetTaskSchedulerToPrepareResourcesForConcurrentAccess(TResource resource) + { + return TaskScheduler.Default; + } + + /// + /// Prepares a resource for concurrent access. + /// + /// The resource to prepare. + /// The token whose cancellation signals lost interest in the resource. + /// A task whose completion signals the resource has been prepared. + /// + /// This is invoked on a resource when it is initially requested for concurrent access, + /// for both transitions from no access and exclusive access. + /// + protected abstract Task PrepareResourceForConcurrentAccessAsync(TResource resource, CancellationToken cancellationToken); + + /// + /// Prepares a resource for access by one thread. + /// + /// The resource to prepare. + /// The aggregate of all flags from the active and nesting locks. + /// The token whose cancellation signals lost interest in the resource. + /// A task whose completion signals the resource has been prepared. + /// + /// This is invoked on a resource when it is initially access for exclusive access, + /// but only when transitioning from no access -- it is not invoked when transitioning + /// from concurrent access to exclusive access. + /// + protected abstract Task PrepareResourceForExclusiveAccessAsync(TResource resource, LockFlags lockFlags, CancellationToken cancellationToken); + + /// + /// Invoked after an exclusive lock is released but before anyone has a chance to enter the lock. + /// + /// + /// This method is called while holding a private lock in order to block future lock consumers till this method is finished. + /// + protected override async Task OnExclusiveLockReleasedAsync() + { + await base.OnExclusiveLockReleasedAsync().ConfigureAwait(false); + await this.helper.OnExclusiveLockReleasedAsync().ConfigureAwait(false); + } /// - /// A non-blocking lock that allows concurrent access, exclusive access, or concurrent with upgradeability to exclusive access, - /// making special allowances for resources that must be prepared for concurrent or exclusive access. + /// Invoked when a top-level upgradeable read lock is released, leaving no remaining (write) lock. /// - /// The type of the moniker that identifies a resource. - /// The type of resource issued for access by this lock. - public abstract class AsyncReaderWriterResourceLock : AsyncReaderWriterLock - where TResource : class + protected override void OnUpgradeableReadLockReleased() { + base.OnUpgradeableReadLockReleased(); + this.helper.OnUpgradeableReadLockReleased(); + } + + /// + /// An awaitable that is returned from asynchronous lock requests. + /// + public readonly struct ResourceAwaitable + { + /// + /// The underlying lock awaitable. + /// + private readonly AsyncReaderWriterLock.Awaitable awaitable; + /// - /// A private nested class we use to isolate some of the behavior. + /// The helper class. /// private readonly Helper helper; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the struct. /// - protected AsyncReaderWriterResourceLock() + /// The underlying lock awaitable. + /// The helper class. + internal ResourceAwaitable(AsyncReaderWriterLock.Awaitable awaitable, Helper helper) { - this.helper = new Helper(this); + this.awaitable = awaitable; + this.helper = helper; } /// - /// Initializes a new instance of the class. + /// Gets the awaiter value. /// - /// - /// true to spend additional resources capturing diagnostic details that can be used - /// to analyze deadlocks or other issues. - protected AsyncReaderWriterResourceLock(bool captureDiagnostics) - : base(captureDiagnostics) + public ResourceAwaiter GetAwaiter() { - this.helper = new Helper(this); + return new ResourceAwaiter(this.awaitable.GetAwaiter(), this.helper); } + } + /// + /// Manages asynchronous access to a lock. + /// + [DebuggerDisplay("{awaiter.kind}")] + public readonly struct ResourceAwaiter : ICriticalNotifyCompletion + { /// - /// Initializes a new instance of the class. + /// The underlying lock awaiter. /// - /// - /// A JoinableTaskContext to help resolve dead locks caused by interdependency between top read lock tasks when there is a pending write lock blocking one of them. - /// - /// - /// true to spend additional resources capturing diagnostic details that can be used - /// to analyze deadlocks or other issues. - protected AsyncReaderWriterResourceLock(JoinableTaskContext? joinableTaskContext, bool captureDiagnostics) - : base(joinableTaskContext, captureDiagnostics) - { - this.helper = new Helper(this); - } + private readonly AsyncReaderWriterLock.Awaiter awaiter; /// - /// Flags that modify default lock behavior. + /// The helper class. /// - [Flags] - public new enum LockFlags - { - /// - /// The default behavior applies. - /// - None = 0x0, + private readonly Helper helper; - /// - /// Causes an upgradeable reader to remain in an upgraded-write state once upgraded, - /// even after the nested write lock has been released. - /// - /// - /// This is useful when you have a batch of possible write operations to apply, which - /// may or may not actually apply in the end, but if any of them change anything, - /// all of their changes should be seen atomically (within a single write lock). - /// This approach is preferable to simply acquiring a write lock around the batch of - /// potential changes because it doesn't defeat concurrent readers until it knows there - /// is a change to actually make. - /// - StickyWrite = 0x1, + /// + /// Initializes a new instance of the struct. + /// + /// The underlying lock awaiter. + /// The helper class. + internal ResourceAwaiter(AsyncReaderWriterLock.Awaiter awaiter, Helper helper) + { + Requires.NotNull(awaiter, nameof(awaiter)); + Requires.NotNull(helper, nameof(helper)); - /// - /// Skips a step to make sure that the resource is initially prepared when retrieved using GetResourceAsync. - /// - /// - /// This flag is dormant for non-write locks. But if present on an upgradeable read lock, - /// this flag will activate for a nested write lock. - /// - SkipInitialPreparation = 0x1000, + this.awaiter = awaiter; + this.helper = helper; } /// - /// Obtains a read lock, asynchronously awaiting for the lock if it is not immediately available. + /// Gets a value indicating whether the lock has been issued. /// - /// - /// A token whose cancellation indicates lost interest in obtaining the lock. - /// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available, - /// a canceled token will cause the code that is waiting for the lock to resume with an . - /// - /// An awaitable object whose result is the lock releaser. - public new ResourceAwaitable ReadLockAsync(CancellationToken cancellationToken = default(CancellationToken)) + public bool IsCompleted { - return new ResourceAwaitable(base.ReadLockAsync(cancellationToken), this.helper); + get + { + if (this.awaiter is null) + { + throw new InvalidOperationException(); + } + + return this.awaiter.IsCompleted; + } } /// - /// Obtains a read lock, asynchronously awaiting for the lock if it is not immediately available. + /// Sets the delegate to execute when the lock is available. /// - /// Modifications to normal lock behavior. - /// - /// A token whose cancellation indicates lost interest in obtaining the lock. - /// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available, - /// a canceled token will cause the code that is waiting for the lock to resume with an . - /// - /// An awaitable object whose result is the lock releaser. - public ResourceAwaitable UpgradeableReadLockAsync(LockFlags options, CancellationToken cancellationToken = default(CancellationToken)) + /// The delegate. + public void OnCompleted(Action continuation) { - return new ResourceAwaitable(this.UpgradeableReadLockAsync((AsyncReaderWriterLock.LockFlags)options, cancellationToken), this.helper); + if (this.awaiter is null) + { + throw new InvalidOperationException(); + } + + this.awaiter.OnCompleted(continuation); } /// - /// Obtains an upgradeable read lock, asynchronously awaiting for the lock if it is not immediately available. + /// Sets the delegate to execute when the lock is available. /// - /// - /// A token whose cancellation indicates lost interest in obtaining the lock. - /// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available, - /// a canceled token will cause the code that is waiting for the lock to resume with an . - /// - /// An awaitable object whose result is the lock releaser. - public new ResourceAwaitable UpgradeableReadLockAsync(CancellationToken cancellationToken = default(CancellationToken)) + /// The delegate. + public void UnsafeOnCompleted(Action continuation) { - return new ResourceAwaitable(base.UpgradeableReadLockAsync(cancellationToken), this.helper); + if (this.awaiter is null) + { + throw new InvalidOperationException(); + } + + this.awaiter.UnsafeOnCompleted(continuation); } /// - /// Obtains a write lock, asynchronously awaiting for the lock if it is not immediately available. + /// Applies the issued lock to the caller and returns the value used to release the lock. /// - /// - /// A token whose cancellation indicates lost interest in obtaining the lock. - /// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available, - /// a canceled token will cause the code that is waiting for the lock to resume with an . - /// - /// An awaitable object whose result is the lock releaser. - public new ResourceAwaitable WriteLockAsync(CancellationToken cancellationToken = default(CancellationToken)) + /// The value to dispose of to release the lock. + public ResourceReleaser GetResult() { - return new ResourceAwaitable(base.WriteLockAsync(cancellationToken), this.helper); + if (this.awaiter is null) + { + throw new InvalidOperationException(); + } + + return new ResourceReleaser(this.awaiter.GetResult(), this.helper); } + } + /// + /// A value whose disposal releases a held lock. + /// + [DebuggerDisplay("{releaser.awaiter.kind}")] + public readonly struct ResourceReleaser : IDisposable, System.IAsyncDisposable + { /// - /// Obtains a write lock, asynchronously awaiting for the lock if it is not immediately available. + /// The underlying lock releaser. /// - /// Modifications to normal lock behavior. - /// - /// A token whose cancellation indicates lost interest in obtaining the lock. - /// A canceled token does not release a lock that has already been issued. But if the lock isn't immediately available, - /// a canceled token will cause the code that is waiting for the lock to resume with an . - /// - /// An awaitable object whose result is the lock releaser. - public ResourceAwaitable WriteLockAsync(LockFlags options, CancellationToken cancellationToken = default(CancellationToken)) - { - return new ResourceAwaitable(this.WriteLockAsync((AsyncReaderWriterLock.LockFlags)options, cancellationToken), this.helper); - } + private readonly AsyncReaderWriterLock.Releaser releaser; /// - /// Retrieves the resource with the specified moniker. + /// The helper class. /// - /// The identifier for the desired resource. - /// A token whose cancellation indicates lost interest in obtaining the resource. - /// A task whose result is the desired resource. - protected abstract Task GetResourceAsync(TMoniker resourceMoniker, CancellationToken cancellationToken); + private readonly Helper helper; /// - /// Marks a resource as having been retrieved under a lock. + /// Initializes a new instance of the struct. /// - protected void SetResourceAsAccessed(TResource resource) + /// The underlying lock releaser. + /// The helper class. + internal ResourceReleaser(AsyncReaderWriterLock.Releaser releaser, Helper helper) { - this.helper.SetResourceAsAccessed(resource); + this.releaser = releaser; + this.helper = helper; } /// - /// Marks any loaded resources as having been retrieved under a lock if they - /// satisfy some predicate. + /// Gets the underlying lock releaser. /// - /// A function that returns true if the provided resource should be considered retrieved. - /// The state object to pass as a second parameter to . - /// true if the delegate returned true on any of the invocations. - protected bool SetResourceAsAccessed(Func resourceCheck, object? state) + internal AsyncReaderWriterLock.Releaser LockReleaser { - return this.helper.SetResourceAsAccessed(resourceCheck, state); + get { return this.releaser; } } /// - /// Sets all the resources to be considered in an unknown state. + /// Gets the lock protected resource. /// - protected void SetAllResourcesToUnknownState() + /// The identifier for the protected resource. + /// A token whose cancellation signals lost interest in the protected resource. + /// A task whose result is the resource. + public Task GetResourceAsync(TMoniker resourceMoniker, CancellationToken cancellationToken = default(CancellationToken)) { - Verify.Operation(this.IsWriteLockHeld, Strings.InvalidLock); - this.helper.SetAllResourcesToUnknownState(); + return this.helper.GetResourceAsync(resourceMoniker, cancellationToken); } /// - /// Returns the aggregate of the lock flags for all nested locks. + /// Releases the lock. /// - protected new LockFlags GetAggregateLockFlags() + public void Dispose() { - return (LockFlags)base.GetAggregateLockFlags(); + this.LockReleaser.Dispose(); } /// - /// Prepares a resource for concurrent access. + /// Releases the lock. /// - /// The resource to prepare. - /// The token whose cancellation signals lost interest in the resource. - /// A task whose completion signals the resource has been prepared. - /// - /// This is invoked on a resource when it is initially requested for concurrent access, - /// for both transitions from no access and exclusive access. - /// - protected abstract Task PrepareResourceForConcurrentAccessAsync(TResource resource, CancellationToken cancellationToken); + public ValueTask DisposeAsync() => this.LockReleaser.DisposeAsync(); /// - /// Prepares a resource for access by one thread. + /// Asynchronously releases the lock. Dispose should still be called after this. /// - /// The resource to prepare. - /// The aggregate of all flags from the active and nesting locks. - /// The token whose cancellation signals lost interest in the resource. - /// A task whose completion signals the resource has been prepared. /// - /// This is invoked on a resource when it is initially access for exclusive access, - /// but only when transitioning from no access -- it is not invoked when transitioning - /// from concurrent access to exclusive access. + /// Rather than calling this method explicitly, use the C# 8 "await using" syntax instead. /// - protected abstract Task PrepareResourceForExclusiveAccessAsync(TResource resource, LockFlags lockFlags, CancellationToken cancellationToken); + public Task ReleaseAsync() + { + return this.LockReleaser.ReleaseAsync(); + } + } + /// + /// A helper class to isolate some specific functionality in this outer class. + /// + internal class Helper + { /// - /// Invoked after an exclusive lock is released but before anyone has a chance to enter the lock. + /// The owning lock instance. /// - /// - /// This method is called while holding a private lock in order to block future lock consumers till this method is finished. - /// - protected override async Task OnExclusiveLockReleasedAsync() - { - await base.OnExclusiveLockReleasedAsync().ConfigureAwait(false); - await this.helper.OnExclusiveLockReleasedAsync().ConfigureAwait(false); - } + private readonly AsyncReaderWriterResourceLock service; /// - /// Invoked when a top-level upgradeable read lock is released, leaving no remaining (write) lock. + /// A reusable delegate that invokes the method. /// - protected override void OnUpgradeableReadLockReleased() - { - base.OnUpgradeableReadLockReleased(); - this.helper.OnUpgradeableReadLockReleased(); - } + private readonly Func prepareResourceConcurrentDelegate; /// - /// An awaitable that is returned from asynchronous lock requests. + /// A reusable delegate that invokes the method. /// - public readonly struct ResourceAwaitable - { - /// - /// The underlying lock awaitable. - /// - private readonly AsyncReaderWriterLock.Awaitable awaitable; + private readonly Func prepareResourceExclusiveDelegate; - /// - /// The helper class. - /// - private readonly Helper helper; + /// + /// A reusable delegate that invokes the method. + /// + private readonly Func prepareResourceConcurrentContinuationDelegate; - /// - /// Initializes a new instance of the struct. - /// - /// The underlying lock awaitable. - /// The helper class. - internal ResourceAwaitable(AsyncReaderWriterLock.Awaitable awaitable, Helper helper) - { - this.awaitable = awaitable; - this.helper = helper; - } + /// + /// A reusable delegate that invokes the method. + /// + private readonly Func prepareResourceExclusiveContinuationDelegate; - /// - /// Gets the awaiter value. - /// - public ResourceAwaiter GetAwaiter() - { - return new ResourceAwaiter(this.awaitable.GetAwaiter(), this.helper); - } - } + /// + /// A reusable delegate that invokes the method. + /// + private readonly Func prepareResourceConcurrentContinuationOnPossibleCancelledTaskDelegate; /// - /// Manages asynchronous access to a lock. + /// A reusable delegate that invokes the method. /// - [DebuggerDisplay("{awaiter.kind}")] - public readonly struct ResourceAwaiter : ICriticalNotifyCompletion - { - /// - /// The underlying lock awaiter. - /// - private readonly AsyncReaderWriterLock.Awaiter awaiter; + private readonly Func prepareResourceExclusiveContinuationOnPossibleCancelledTaskDelegateDelegate; - /// - /// The helper class. - /// - private readonly Helper helper; + /// + /// A collection of all the resources requested within the outermost upgradeable read lock. + /// + private readonly HashSet resourcesAcquiredWithinUpgradeableRead = new HashSet(); - /// - /// Initializes a new instance of the struct. - /// - /// The underlying lock awaiter. - /// The helper class. - internal ResourceAwaiter(AsyncReaderWriterLock.Awaiter awaiter, Helper helper) - { - Requires.NotNull(awaiter, nameof(awaiter)); - Requires.NotNull(helper, nameof(helper)); + /// + /// A map of resources to the status of tasks that most recently began evaluating them. + /// + private readonly WeakKeyDictionary resourcePreparationStates = new WeakKeyDictionary(capacity: 2); - this.awaiter = awaiter; - this.helper = helper; - } + /// + /// Initializes a new instance of the class. + /// + /// The owning lock instance. + internal Helper(AsyncReaderWriterResourceLock service) + { + Requires.NotNull(service, nameof(service)); - /// - /// Gets a value indicating whether the lock has been issued. - /// - public bool IsCompleted + this.service = service; + this.prepareResourceConcurrentDelegate = state => { - get - { - if (this.awaiter is null) - { - throw new InvalidOperationException(); - } + var tuple = (Tuple)state; + return this.service.PrepareResourceForConcurrentAccessAsync(tuple.Item1, tuple.Item2); + }; - return this.awaiter.IsCompleted; - } - } + this.prepareResourceExclusiveDelegate = state => + { + var tuple = (Tuple)state; + return this.service.PrepareResourceForExclusiveAccessAsync(tuple.Item1, tuple.Item2, tuple.Item3); + }; - /// - /// Sets the delegate to execute when the lock is available. - /// - /// The delegate. - public void OnCompleted(Action continuation) + this.prepareResourceConcurrentContinuationDelegate = (prev, state) => { - if (this.awaiter is null) - { - throw new InvalidOperationException(); - } + var tuple = (Tuple)state; + return this.service.PrepareResourceForConcurrentAccessAsync(tuple.Item1, tuple.Item2); + }; - this.awaiter.OnCompleted(continuation); - } + this.prepareResourceExclusiveContinuationDelegate = (prev, state) => + { + var tuple = (Tuple)state; + return this.service.PrepareResourceForExclusiveAccessAsync(tuple.Item1, tuple.Item2, tuple.Item3); + }; - /// - /// Sets the delegate to execute when the lock is available. - /// - /// The delegate. - public void UnsafeOnCompleted(Action continuation) + // this delegate is to handle the case that we prepare resource when the previous task might be cancelled. + // Because the previous task might not be cancelled, but actually finished. In that case, we will consider the work has done, and there is no need to prepare it again. + this.prepareResourceConcurrentContinuationOnPossibleCancelledTaskDelegate = (prev, state) => { - if (this.awaiter is null) + if (!prev.IsFaulted && !prev.IsCanceled) { - throw new InvalidOperationException(); + return prev; } - this.awaiter.UnsafeOnCompleted(continuation); - } + var tuple = (Tuple)state; + return this.service.PrepareResourceForConcurrentAccessAsync(tuple.Item1, tuple.Item2); + }; - /// - /// Applies the issued lock to the caller and returns the value used to release the lock. - /// - /// The value to dispose of to release the lock. - public ResourceReleaser GetResult() + this.prepareResourceExclusiveContinuationOnPossibleCancelledTaskDelegateDelegate = (prev, state) => { - if (this.awaiter is null) + if (!prev.IsFaulted && !prev.IsCanceled) { - throw new InvalidOperationException(); + return prev; } - return new ResourceReleaser(this.awaiter.GetResult(), this.helper); - } + var tuple = (Tuple)state; + return this.service.PrepareResourceForExclusiveAccessAsync(tuple.Item1, tuple.Item2, tuple.Item3); + }; } /// - /// A value whose disposal releases a held lock. + /// Describes the states a resource can be in. /// - [DebuggerDisplay("{releaser.awaiter.kind}")] - public readonly struct ResourceReleaser : IDisposable, System.IAsyncDisposable + private enum ResourceState { /// - /// The underlying lock releaser. + /// The resource is neither prepared for concurrent nor exclusive access. /// - private readonly AsyncReaderWriterLock.Releaser releaser; + Unknown, /// - /// The helper class. + /// The resource is prepared for concurrent access. /// - private readonly Helper helper; - - /// - /// Initializes a new instance of the struct. - /// - /// The underlying lock releaser. - /// The helper class. - internal ResourceReleaser(AsyncReaderWriterLock.Releaser releaser, Helper helper) - { - this.releaser = releaser; - this.helper = helper; - } + Concurrent, /// - /// Gets the underlying lock releaser. + /// The resource is prepared for exclusive access. /// - internal AsyncReaderWriterLock.Releaser LockReleaser - { - get { return this.releaser; } - } + Exclusive, + } - /// - /// Gets the lock protected resource. - /// - /// The identifier for the protected resource. - /// A token whose cancellation signals lost interest in the protected resource. - /// A task whose result is the resource. - public Task GetResourceAsync(TMoniker resourceMoniker, CancellationToken cancellationToken = default(CancellationToken)) + /// + /// Marks a resource as having been retrieved under a lock. + /// + internal void SetResourceAsAccessed(TResource resource) + { + Requires.NotNull(resource, nameof(resource)); + + // Capture the ambient lock and use it for the two lock checks rather than + // call AsyncReaderWriterLock.IsWriteLockHeld and IsUpgradeableReadLockHeld + // to reduce the number of slow AsyncLocal.get_Value calls we make. + // Also do it before we acquire the lock, since a lock isn't necessary. + // (verified to be a perf bottleneck in ETL traces). + LockHandle ambientLock = this.service.AmbientLock; + lock (this.service.SyncObject) { - return this.helper.GetResourceAsync(resourceMoniker, cancellationToken); + if (!ambientLock.HasWriteLock && ambientLock.HasUpgradeableReadLock) + { + this.resourcesAcquiredWithinUpgradeableRead.Add(resource); + } } + } - /// - /// Releases the lock. - /// - public void Dispose() + /// + /// Marks any loaded resources as having been retrieved under a lock if they + /// satisfy some predicate. + /// + /// A function that returns if the provided resource should be considered retrieved. + /// The state object to pass as a second parameter to . + /// if the delegate returned on any of the invocations. + internal bool SetResourceAsAccessed(Func resourceCheck, object? state) + { + Requires.NotNull(resourceCheck, nameof(resourceCheck)); + + // Capture the ambient lock and use it for the two lock checks rather than + // call AsyncReaderWriterLock.IsWriteLockHeld and IsUpgradeableReadLockHeld + // to reduce the number of slow AsyncLocal.get_Value calls we make. + // Also do it before we acquire the lock, since a lock isn't necessary. + // (verified to be a perf bottleneck in ETL traces). + LockHandle ambientLock = this.service.AmbientLock; + bool match = false; + lock (this.service.SyncObject) { - this.LockReleaser.Dispose(); + if (ambientLock.HasWriteLock || ambientLock.HasUpgradeableReadLock) + { + foreach (KeyValuePair.Helper.ResourcePreparationTaskState> resource in this.resourcePreparationStates) + { + if (resourceCheck(resource.Key, state)) + { + match = true; + this.SetResourceAsAccessed(resource.Key); + } + } + } } - /// - /// Releases the lock. - /// - public ValueTask DisposeAsync() => this.LockReleaser.DisposeAsync(); - - /// - /// Asynchronously releases the lock. Dispose should still be called after this. - /// - /// - /// Rather than calling this method explicitly, use the C# 8 "await using" syntax instead. - /// - public Task ReleaseAsync() - { - return this.LockReleaser.ReleaseAsync(); - } + return match; } /// - /// A helper class to isolate some specific functionality in this outer class. + /// Ensures that all resources are marked as unprepared so at next request they are prepared again. /// - internal class Helper + internal Task OnExclusiveLockReleasedAsync() { - /// - /// The owning lock instance. - /// - private readonly AsyncReaderWriterResourceLock service; - - /// - /// A reusable delegate that invokes the method. - /// - private readonly Func prepareResourceConcurrentDelegate; - - /// - /// A reusable delegate that invokes the method. - /// - private readonly Func prepareResourceExclusiveDelegate; - - /// - /// A reusable delegate that invokes the method. - /// - private readonly Func prepareResourceConcurrentContinuationDelegate; - - /// - /// A reusable delegate that invokes the method. - /// - private readonly Func prepareResourceExclusiveContinuationDelegate; - - /// - /// A reusable delegate that invokes the method. - /// - private readonly Func prepareResourceConcurrentContinuationOnPossibleCancelledTaskDelegate; - - /// - /// A reusable delegate that invokes the method. - /// - private readonly Func prepareResourceExclusiveContinuationOnPossibleCancelledTaskDelegateDelegate; - - /// - /// A collection of all the resources requested within the outermost upgradeable read lock. - /// - private readonly HashSet resourcesAcquiredWithinUpgradeableRead = new HashSet(); - - /// - /// A map of resources to the status of tasks that most recently began evaluating them. - /// - private WeakKeyDictionary resourcePreparationStates = new WeakKeyDictionary(capacity: 2); - - /// - /// Initializes a new instance of the class. - /// - /// The owning lock instance. - internal Helper(AsyncReaderWriterResourceLock service) + lock (this.service.SyncObject) { - Requires.NotNull(service, nameof(service)); - - this.service = service; - this.prepareResourceConcurrentDelegate = state => - { - var tuple = (Tuple)state; - return this.service.PrepareResourceForConcurrentAccessAsync(tuple.Item1, tuple.Item2); - }; - - this.prepareResourceExclusiveDelegate = state => - { - var tuple = (Tuple)state; - return this.service.PrepareResourceForExclusiveAccessAsync(tuple.Item1, tuple.Item2, tuple.Item3); - }; - - this.prepareResourceConcurrentContinuationDelegate = (prev, state) => - { - var tuple = (Tuple)state; - return this.service.PrepareResourceForConcurrentAccessAsync(tuple.Item1, tuple.Item2); - }; - - this.prepareResourceExclusiveContinuationDelegate = (prev, state) => - { - var tuple = (Tuple)state; - return this.service.PrepareResourceForExclusiveAccessAsync(tuple.Item1, tuple.Item2, tuple.Item3); - }; + // Reset ALL resources to an unknown state. Not just the ones explicitly requested + // because backdoors can and legitimately do (as in CPS) exist for tampering + // with a resource without going through our access methods. + this.SetAllResourcesToUnknownState(); - // this delegate is to handle the case that we prepare resource when the previous task might be cancelled. - // Because the previous task might not be cancelled, but actually finished. In that case, we will consider the work has done, and there is no need to prepare it again. - this.prepareResourceConcurrentContinuationOnPossibleCancelledTaskDelegate = (prev, state) => + if (this.service.IsUpgradeableReadLockHeld && this.resourcesAcquiredWithinUpgradeableRead.Count > 0) { - if (!prev.IsFaulted && !prev.IsCanceled) + // We must also synchronously prepare all resources that were acquired within the upgradeable read lock + // because as soon as this method returns these resources may be access concurrently again. + var preparationTasks = new Task[this.resourcesAcquiredWithinUpgradeableRead.Count]; + int taskIndex = 0; + foreach (TResource? resource in this.resourcesAcquiredWithinUpgradeableRead) { - return prev; + preparationTasks[taskIndex++] = this.PrepareResourceAsync(resource, CancellationToken.None, forcePrepareConcurrent: true); } - var tuple = (Tuple)state; - return this.service.PrepareResourceForConcurrentAccessAsync(tuple.Item1, tuple.Item2); - }; - - this.prepareResourceExclusiveContinuationOnPossibleCancelledTaskDelegateDelegate = (prev, state) => - { - if (!prev.IsFaulted && !prev.IsCanceled) + if (preparationTasks.Length == 1) { - return prev; + return preparationTasks[0]; } - - var tuple = (Tuple)state; - return this.service.PrepareResourceForExclusiveAccessAsync(tuple.Item1, tuple.Item2, tuple.Item3); - }; - } - - /// - /// Describes the states a resource can be in. - /// - private enum ResourceState - { - /// - /// The resource is neither prepared for concurrent nor exclusive access. - /// - Unknown, - - /// - /// The resource is prepared for concurrent access. - /// - Concurrent, - - /// - /// The resource is prepared for exclusive access. - /// - Exclusive, - } - - /// - /// Marks a resource as having been retrieved under a lock. - /// - internal void SetResourceAsAccessed(TResource resource) - { - Requires.NotNull(resource, nameof(resource)); - - // Capture the ambient lock and use it for the two lock checks rather than - // call AsyncReaderWriterLock.IsWriteLockHeld and IsUpgradeableReadLockHeld - // to reduce the number of slow AsyncLocal.get_Value calls we make. - // Also do it before we acquire the lock, since a lock isn't necessary. - // (verified to be a perf bottleneck in ETL traces). - LockHandle ambientLock = this.service.AmbientLock; - lock (this.service.SyncObject) - { - if (!ambientLock.HasWriteLock && ambientLock.HasUpgradeableReadLock) + else if (preparationTasks.Length > 1) { - this.resourcesAcquiredWithinUpgradeableRead.Add(resource); + return Task.WhenAll(preparationTasks); } } } - /// - /// Marks any loaded resources as having been retrieved under a lock if they - /// satisfy some predicate. - /// - /// A function that returns true if the provided resource should be considered retrieved. - /// The state object to pass as a second parameter to . - /// true if the delegate returned true on any of the invocations. - internal bool SetResourceAsAccessed(Func resourceCheck, object? state) - { - Requires.NotNull(resourceCheck, nameof(resourceCheck)); - - // Capture the ambient lock and use it for the two lock checks rather than - // call AsyncReaderWriterLock.IsWriteLockHeld and IsUpgradeableReadLockHeld - // to reduce the number of slow AsyncLocal.get_Value calls we make. - // Also do it before we acquire the lock, since a lock isn't necessary. - // (verified to be a perf bottleneck in ETL traces). - LockHandle ambientLock = this.service.AmbientLock; - bool match = false; - lock (this.service.SyncObject) - { - if (ambientLock.HasWriteLock || ambientLock.HasUpgradeableReadLock) - { - foreach (KeyValuePair.Helper.ResourcePreparationTaskState> resource in this.resourcePreparationStates) - { - if (resourceCheck(resource.Key, state)) - { - match = true; - this.SetResourceAsAccessed(resource.Key); - } - } - } - } + return Task.CompletedTask; + } - return match; - } + /// + /// Invoked when a top-level upgradeable read lock is released, leaving no remaining (write) lock. + /// + internal void OnUpgradeableReadLockReleased() + { + this.resourcesAcquiredWithinUpgradeableRead.Clear(); + } - /// - /// Ensures that all resources are marked as unprepared so at next request they are prepared again. - /// - internal Task OnExclusiveLockReleasedAsync() + /// + /// Retrieves the resource with the specified moniker. + /// + /// The identifier for the desired resource. + /// The token whose cancellation signals lost interest in this resource. + /// A task whose result is the desired resource. + internal async Task GetResourceAsync(TMoniker resourceMoniker, CancellationToken cancellationToken) + { + using (AsyncReaderWriterResourceLock.ResourceReleaser resourceLock = this.AcquirePreexistingLockOrThrow()) { + TResource? resource = await this.service.GetResourceAsync(resourceMoniker, cancellationToken).ConfigureAwaitRunInline(); + Task preparationTask; + lock (this.service.SyncObject) { - // Reset ALL resources to an unknown state. Not just the ones explicitly requested - // because backdoors can and legitimately do (as in CPS) exist for tampering - // with a resource without going through our access methods. - this.SetAllResourcesToUnknownState(); - - if (this.service.IsUpgradeableReadLockHeld && this.resourcesAcquiredWithinUpgradeableRead.Count > 0) - { - // We must also synchronously prepare all resources that were acquired within the upgradeable read lock - // because as soon as this method returns these resources may be access concurrently again. - var preparationTasks = new Task[this.resourcesAcquiredWithinUpgradeableRead.Count]; - int taskIndex = 0; - foreach (TResource? resource in this.resourcesAcquiredWithinUpgradeableRead) - { - preparationTasks[taskIndex++] = this.PrepareResourceAsync(resource, CancellationToken.None, forcePrepareConcurrent: true); - } + this.SetResourceAsAccessed(resource); - if (preparationTasks.Length == 1) - { - return preparationTasks[0]; - } - else if (preparationTasks.Length > 1) - { - return Task.WhenAll(preparationTasks); - } - } + preparationTask = this.PrepareResourceAsync(resource, cancellationToken); } - return Task.CompletedTask; + await preparationTask.ConfigureAwaitRunInline(); + return resource; } + } - /// - /// Invoked when a top-level upgradeable read lock is released, leaving no remaining (write) lock. - /// - internal void OnUpgradeableReadLockReleased() + /// + /// Sets all the resources to be considered in an unknown state. Any subsequent access (exclusive or concurrent) will prepare the resource. + /// + internal void SetAllResourcesToUnknownState() + { + this.SetUnknownResourceState(this.resourcePreparationStates.Select(rp => rp.Key).ToList()); + } + + /// + /// Sets the specified resource to be considered in an unknown state. Any subsequent access (exclusive or concurrent) will prepare the resource. + /// + private void SetUnknownResourceState(TResource resource) + { + Requires.NotNull(resource, nameof(resource)); + + lock (this.service.SyncObject) { - this.resourcesAcquiredWithinUpgradeableRead.Clear(); + this.resourcePreparationStates.TryGetValue(resource, out ResourcePreparationTaskState? previousState); + this.resourcePreparationStates[resource] = ResourcePreparationTaskState.Create( + _ => previousState?.InnerTask ?? Task.CompletedTask, + ResourceState.Unknown, + TaskScheduler.Default, + CancellationToken.None).PreparationState; } + } - /// - /// Retrieves the resource with the specified moniker. - /// - /// The identifier for the desired resource. - /// The token whose cancellation signals lost interest in this resource. - /// A task whose result is the desired resource. - internal async Task GetResourceAsync(TMoniker resourceMoniker, CancellationToken cancellationToken) + /// + /// Sets the specified resources to be considered in an unknown state. Any subsequent access (exclusive or concurrent) will prepare the resource. + /// + private void SetUnknownResourceState(IEnumerable resources) + { + Requires.NotNull(resources, nameof(resources)); + foreach (TResource? resource in resources) { - using (AsyncReaderWriterResourceLock.ResourceReleaser resourceLock = this.AcquirePreexistingLockOrThrow()) - { - TResource? resource = await this.service.GetResourceAsync(resourceMoniker, cancellationToken).ConfigureAwait(false); - Task preparationTask; + this.SetUnknownResourceState(resource); + } + } - lock (this.service.SyncObject) - { - this.SetResourceAsAccessed(resource); + /// + /// Prepares the specified resource for access by a lock holder. + /// + /// The resource to prepare. + /// The token whose cancellation signals lost interest in this resource. + /// Force preparation of the resource for concurrent access, even if an exclusive lock is currently held. + /// A task that is completed when preparation has completed. + private Task PrepareResourceAsync(TResource resource, CancellationToken cancellationToken, bool forcePrepareConcurrent = false) + { + Requires.NotNull(resource, nameof(resource)); + Assumes.True(Monitor.IsEntered(this.service.SyncObject)); - preparationTask = this.PrepareResourceAsync(resource, cancellationToken); - } + // We deliberately ignore the cancellation token in the tasks we create and save because the tasks can be shared + // across requests and we can't have task continuation chains where tasks within the chain get canceled + // as that can cause premature starting of the next task in the chain. + bool forConcurrentUse = forcePrepareConcurrent || !this.service.IsWriteLockHeld; + AsyncReaderWriterResourceLock.Helper.ResourceState finalState = forConcurrentUse ? ResourceState.Concurrent : ResourceState.Exclusive; - await preparationTask.ConfigureAwait(false); - return resource; - } - } + Task? preparationTask = null; + TaskScheduler taskScheduler = this.service.GetTaskSchedulerToPrepareResourcesForConcurrentAccess(resource); - /// - /// Sets all the resources to be considered in an unknown state. Any subsequent access (exclusive or concurrent) will prepare the resource. - /// - internal void SetAllResourcesToUnknownState() + if (!this.resourcePreparationStates.TryGetValue(resource, out ResourcePreparationTaskState? preparationState)) { - this.SetUnknownResourceState(this.resourcePreparationStates.Select(rp => rp.Key).ToList()); - } - - /// - /// Sets the specified resource to be considered in an unknown state. Any subsequent access (exclusive or concurrent) will prepare the resource. - /// - private void SetUnknownResourceState(TResource resource) - { - Requires.NotNull(resource, nameof(resource)); - - lock (this.service.SyncObject) + Func? preparationDelegate = forConcurrentUse + ? this.prepareResourceConcurrentDelegate + : this.prepareResourceExclusiveDelegate; + + // We kick this off on a new task because we're currently holding a private lock + // and don't want to execute arbitrary code. + // Let's also hide the ARWL from the delegate if this is a shared lock request. + using (forConcurrentUse ? this.service.HideLocks() : default) { - this.resourcePreparationStates.TryGetValue(resource, out ResourcePreparationTaskState? previousState); - this.resourcePreparationStates[resource] = ResourcePreparationTaskState.Create( - _ => previousState?.InnerTask ?? Task.CompletedTask, - ResourceState.Unknown, - CancellationToken.None).PreparationState; + // We can't currently use the caller's cancellation token for this task because + // this task may be shared with others or call this method later, and we wouldn't + // want their requests to be cancelled as a result of this first caller cancelling. + (preparationState, preparationTask) = ResourcePreparationTaskState.Create( + combinedCancellationToken => Task.Factory.StartNew( + NullableHelpers.AsNullableArgFunc(preparationDelegate), + forConcurrentUse ? Tuple.Create(resource, combinedCancellationToken) : Tuple.Create(resource, this.service.GetAggregateLockFlags(), combinedCancellationToken), + combinedCancellationToken, + TaskCreationOptions.None, + taskScheduler).Unwrap(), + finalState, + taskScheduler, + cancellationToken); } } - - /// - /// Sets the specified resources to be considered in an unknown state. Any subsequent access (exclusive or concurrent) will prepare the resource. - /// - private void SetUnknownResourceState(IEnumerable resources) + else { - Requires.NotNull(resources, nameof(resources)); - foreach (TResource? resource in resources) + Func? preparationDelegate = null; + if (preparationState.State != finalState || preparationState.InnerTask.IsFaulted) { - this.SetUnknownResourceState(resource); + preparationDelegate = forConcurrentUse + ? this.prepareResourceConcurrentContinuationDelegate + : this.prepareResourceExclusiveContinuationDelegate; + } + else if (!preparationState.TryJoinPreparationTask(out preparationTask, taskScheduler, cancellationToken)) + { + preparationDelegate = forConcurrentUse + ? this.prepareResourceConcurrentContinuationOnPossibleCancelledTaskDelegate + : this.prepareResourceExclusiveContinuationOnPossibleCancelledTaskDelegateDelegate; } - } - - /// - /// Prepares the specified resource for access by a lock holder. - /// - /// The resource to prepare. - /// The token whose cancellation signals lost interest in this resource. - /// Force preparation of the resource for concurrent access, even if an exclusive lock is currently held. - /// A task that is completed when preparation has completed. - private Task PrepareResourceAsync(TResource resource, CancellationToken cancellationToken, bool forcePrepareConcurrent = false) - { - Requires.NotNull(resource, nameof(resource)); - Assumes.True(Monitor.IsEntered(this.service.SyncObject)); - - // We deliberately ignore the cancellation token in the tasks we create and save because the tasks can be shared - // across requests and we can't have task continuation chains where tasks within the chain get canceled - // as that can cause premature starting of the next task in the chain. - bool forConcurrentUse = forcePrepareConcurrent || !this.service.IsWriteLockHeld; - AsyncReaderWriterResourceLock.Helper.ResourceState finalState = forConcurrentUse ? ResourceState.Concurrent : ResourceState.Exclusive; - - Task? preparationTask = null; - if (!this.resourcePreparationStates.TryGetValue(resource, out ResourcePreparationTaskState? preparationState)) + if (preparationTask is null) { - Func? preparationDelegate = forConcurrentUse - ? this.prepareResourceConcurrentDelegate - : this.prepareResourceExclusiveDelegate; + Assumes.NotNull(preparationDelegate); // We kick this off on a new task because we're currently holding a private lock // and don't want to execute arbitrary code. // Let's also hide the ARWL from the delegate if this is a shared lock request. - using (forConcurrentUse ? this.service.HideLocks() : default(Suppression)) + using (forConcurrentUse ? this.service.HideLocks() : default) { - // We can't currently use the caller's cancellation token for this task because - // this task may be shared with others or call this method later, and we wouldn't - // want their requests to be cancelled as a result of this first caller cancelling. (preparationState, preparationTask) = ResourcePreparationTaskState.Create( - combinedCancellationToken => Task.Factory.StartNew( - NullableHelpers.AsNullableArgFunc(preparationDelegate), + combinedCancellationToken => preparationState.InnerTask.ContinueWith( + preparationDelegate!, forConcurrentUse ? Tuple.Create(resource, combinedCancellationToken) : Tuple.Create(resource, this.service.GetAggregateLockFlags(), combinedCancellationToken), - combinedCancellationToken, - TaskCreationOptions.None, - TaskScheduler.Default).Unwrap(), + CancellationToken.None, + TaskContinuationOptions.RunContinuationsAsynchronously, + taskScheduler).Unwrap(), finalState, + taskScheduler, cancellationToken); } } - else - { - Func? preparationDelegate = null; - if (preparationState.State != finalState || preparationState.InnerTask.IsFaulted) - { - preparationDelegate = forConcurrentUse - ? this.prepareResourceConcurrentContinuationDelegate - : this.prepareResourceExclusiveContinuationDelegate; - } - else if (!preparationState.TryJoinPrepationTask(out preparationTask, cancellationToken)) - { - preparationDelegate = forConcurrentUse - ? this.prepareResourceConcurrentContinuationOnPossibleCancelledTaskDelegate - : this.prepareResourceExclusiveContinuationOnPossibleCancelledTaskDelegateDelegate; - } - - if (preparationTask is null) - { - Assumes.NotNull(preparationDelegate); + } - // We kick this off on a new task because we're currently holding a private lock - // and don't want to execute arbitrary code. - // Let's also hide the ARWL from the delegate if this is a shared lock request. - using (forConcurrentUse ? this.service.HideLocks() : default(Suppression)) - { - (preparationState, preparationTask) = ResourcePreparationTaskState.Create( - combinedCancellationToken => preparationState.InnerTask.ContinueWith( - preparationDelegate!, - forConcurrentUse ? Tuple.Create(resource, combinedCancellationToken) : Tuple.Create(resource, this.service.GetAggregateLockFlags(), combinedCancellationToken), - CancellationToken.None, - TaskContinuationOptions.RunContinuationsAsynchronously, - TaskScheduler.Default).Unwrap(), - finalState, - cancellationToken); - } - } - } + Assumes.NotNull(preparationState); + this.resourcePreparationStates[resource] = preparationState; - Assumes.NotNull(preparationState); - this.resourcePreparationStates[resource] = preparationState; + return preparationTask; + } - return preparationTask; + /// + /// Reserves a read lock from a previously held lock. + /// + /// The releaser for the read lock. + /// Thrown if no lock is held by the caller. + private ResourceReleaser AcquirePreexistingLockOrThrow() + { + if (!this.service.IsAnyLockHeld) + { + Verify.FailOperation(Strings.InvalidWithoutLock); } + AsyncReaderWriterResourceLock.ResourceAwaiter awaiter = this.service.ReadLockAsync(CancellationToken.None).GetAwaiter(); + Assumes.True(awaiter.IsCompleted); + return awaiter.GetResult(); + } + + /// + /// Tracks a task that prepares a resource for either concurrent or exclusive use. + /// + private class ResourcePreparationTaskState : CancellableJoinComputation + { /// - /// Reserves a read lock from a previously held lock. + /// Initializes a new instance of the class. /// - /// The releaser for the read lock. - /// Thrown if no lock is held by the caller. - private ResourceReleaser AcquirePreexistingLockOrThrow() + internal ResourcePreparationTaskState(Func taskCreation, ResourceState finalState, bool canBeCancelled) + : base(taskCreation, canBeCancelled) { - if (!this.service.IsAnyLockHeld) - { - Verify.FailOperation(Strings.InvalidWithoutLock); - } - - AsyncReaderWriterResourceLock.ResourceAwaiter awaiter = this.service.ReadLockAsync(CancellationToken.None).GetAwaiter(); - Assumes.True(awaiter.IsCompleted); - return awaiter.GetResult(); + this.State = finalState; } /// - /// Tracks a task that prepares a resource for either concurrent or exclusive use. + /// Gets the state the resource will be in when inner task has completed. /// - private class ResourcePreparationTaskState : CancellableJoinComputation - { - /// - /// Initializes a new instance of the class. - /// - internal ResourcePreparationTaskState(Func taskCreation, ResourceState finalState, bool canBeCancelled) - : base(taskCreation, canBeCancelled) - { - this.State = finalState; - } + internal ResourceState State { get; } - /// - /// Gets the state the resource will be in when inner task has completed. - /// - internal ResourceState State { get; } - - /// - /// Creates a task to prepare the source and returns it with . - /// - /// A callback method to create the preparation task. - /// The final resource state when the preparation is done. - /// A cancellation token to abort the preparation task. - /// The preparation task and its status to be used to join more waiting tasks later. - internal static (ResourcePreparationTaskState PreparationState, Task InitialTask) Create(Func taskCreation, ResourceState finalState, CancellationToken cancellationToken) - { - var preparationState = new ResourcePreparationTaskState(taskCreation, finalState, cancellationToken.CanBeCanceled); - Assumes.True(preparationState.TryJoinComputation(isInitialTask: true, out Task? initialTask, cancellationToken)); + /// + /// Creates a task to prepare the source and returns it with . + /// + /// A callback method to create the preparation task. + /// The final resource state when the preparation is done. + /// A task scheduler for continuation. + /// A cancellation token to abort the preparation task. + /// The preparation task and its status to be used to join more waiting tasks later. + internal static (ResourcePreparationTaskState PreparationState, Task InitialTask) Create(Func taskCreation, ResourceState finalState, TaskScheduler taskScheduler, CancellationToken cancellationToken) + { + var preparationState = new ResourcePreparationTaskState(taskCreation, finalState, cancellationToken.CanBeCanceled); + Assumes.True(preparationState.TryJoinComputation(isInitialTask: true, out Task? initialTask, taskScheduler, cancellationToken)); - return (preparationState, initialTask); - } + return (preparationState, initialTask); + } - /// - /// Try to join an existing preparation task. - /// - /// The new waiting task to be compeleted when the resource preparation is done. - /// A cancellation token to abandone the new waiting task. - /// True if it joins sucessfully, it return false, if the current task has been cancelled. - internal bool TryJoinPrepationTask([NotNullWhen(true)] out Task? task, CancellationToken cancellationToken) - { - return this.TryJoinComputation(isInitialTask: false, out task, cancellationToken); - } + /// + /// Try to join an existing preparation task. + /// + /// The new waiting task to be completed when the resource preparation is done. + /// A task scheduler for continuation. + /// A cancellation token to abandon the new waiting task. + /// True if it joins successfully, it return false, if the current task has been cancelled. + internal bool TryJoinPreparationTask([NotNullWhen(true)] out Task? task, TaskScheduler taskScheduler, CancellationToken cancellationToken) + { + return this.TryJoinComputation(isInitialTask: false, out task, taskScheduler, cancellationToken); } } } diff --git a/src/Microsoft.VisualStudio.Threading/AsyncSemaphore.cs b/src/Microsoft.VisualStudio.Threading/AsyncSemaphore.cs index 8df00e8ed..faab0634f 100644 --- a/src/Microsoft.VisualStudio.Threading/AsyncSemaphore.cs +++ b/src/Microsoft.VisualStudio.Threading/AsyncSemaphore.cs @@ -1,361 +1,379 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// An asynchronous like class with more convenient release syntax. +/// +/// +/// This semaphore guarantees FIFO ordering. +/// +/// This object does *not* need to be disposed of, as it does not hold unmanaged resources. +/// Disposing this object has no effect on current users of the semaphore, and they are allowed to release their hold on the semaphore without exception. +/// An is thrown back at anyone asking to or waiting to enter the semaphore after is called. +/// +/// +public class AsyncSemaphore : IDisposable { - using System; - using System.Collections.Generic; - using System.Threading; - using System.Threading.Tasks; + /// + /// A task that is faulted with an . + /// + private static readonly Task DisposedReleaserTask = TplExtensions.FaultedTask(new ObjectDisposedException(typeof(AsyncSemaphore).FullName)); /// - /// An asynchronous like class with more convenient release syntax. + /// A task that is canceled without a specific token. /// - /// - /// This semaphore guarantees FIFO ordering. - /// - /// This object does *not* need to be disposed of, as it does not hold unmanaged resources. - /// Disposing this object has no effect on current users of the semaphore, and they are allowed to release their hold on the semaphore without exception. - /// An is thrown back at anyone asking to or waiting to enter the semaphore after is called. - /// - /// - public class AsyncSemaphore : IDisposable - { - /// - /// A task that is faulted with an . - /// - private static readonly Task DisposedReleaserTask = TplExtensions.FaultedTask(new ObjectDisposedException(typeof(AsyncSemaphore).FullName)); + private static readonly Task CanceledReleaser = Task.FromCanceled(new CancellationToken(true)); - /// - /// A task that is canceled without a specific token. - /// - private static readonly Task CanceledReleaser = Task.FromCanceled(new CancellationToken(true)); + /// + /// A task to return for any uncontested request for the lock. + /// + private readonly Task uncontestedReleaser; - /// - /// A task to return for any uncontested request for the lock. - /// - private readonly Task uncontestedReleaser; + /// + /// The sync object to lock on for mutable field access. + /// + private readonly object syncObject = new object(); - /// - /// The sync object to lock on for mutable field access. - /// - private readonly object syncObject = new object(); + /// + /// A queue of operations waiting to enter the semaphore. + /// + private readonly LinkedList waiters = new LinkedList(); - /// - /// A queue of operations waiting to enter the semaphore. - /// - private readonly LinkedList waiters = new LinkedList(); + /// + /// A pool of recycled nodes. + /// + private readonly Stack> nodePool = new Stack>(); - /// - /// A pool of recycled nodes. - /// - private readonly Stack> nodePool = new Stack>(); + /// + /// A value indicating whether this instance has been disposed. + /// + private bool disposed; - /// - /// A value indicating whether this instance has been disposed. - /// - private bool disposed; + /// + /// Initializes a new instance of the class. + /// + /// The initial number of requests for the semaphore that can be granted concurrently. + public AsyncSemaphore(int initialCount) + { + this.CurrentCount = initialCount; + this.uncontestedReleaser = Task.FromResult(new Releaser(this)); + } - /// - /// Initializes a new instance of the class. - /// - /// The initial number of requests for the semaphore that can be granted concurrently. - public AsyncSemaphore(int initialCount) - { - this.CurrentCount = initialCount; - this.uncontestedReleaser = Task.FromResult(new Releaser(this)); - } + /// + /// Gets the number of openings that remain in the semaphore. + /// + public int CurrentCount { get; private set; } - /// - /// Gets the number of openings that remain in the semaphore. - /// - public int CurrentCount { get; private set; } + /// + /// Requests access to the lock. + /// + /// A token whose cancellation signals lost interest in the lock. + /// + /// A task whose result is a releaser that should be disposed to release the lock. + /// This task may be canceled if is signaled. + /// + /// Thrown when is canceled before semaphore access is granted. + /// Thrown when this semaphore is disposed before semaphore access is granted. + public Task EnterAsync(CancellationToken cancellationToken = default) => this.EnterAsync(Timeout.InfiniteTimeSpan, cancellationToken); - /// - /// Requests access to the lock. - /// - /// A token whose cancellation signals lost interest in the lock. - /// - /// A task whose result is a releaser that should be disposed to release the lock. - /// This task may be canceled if is signaled. - /// - /// Thrown when is canceled before semaphore access is granted. - /// Thrown when this semaphore is disposed before semaphore access is granted. - public Task EnterAsync(CancellationToken cancellationToken = default) => this.EnterAsync(Timeout.InfiniteTimeSpan, cancellationToken); + /// + /// Requests access to the lock. + /// + /// A timeout for waiting for the lock. + /// A token whose cancellation signals lost interest in the lock. + /// + /// A task whose result is a releaser that should be disposed to release the lock. + /// This task may be canceled if is signaled or expires. + /// + /// Thrown when is canceled or the expires before semaphore access is granted. + /// Thrown when this semaphore is disposed before semaphore access is granted. + public Task EnterAsync(TimeSpan timeout, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } - /// - /// Requests access to the lock. - /// - /// A timeout for waiting for the lock. - /// A token whose cancellation signals lost interest in the lock. - /// - /// A task whose result is a releaser that should be disposed to release the lock. - /// This task may be canceled if is signaled or expires. - /// - /// Thrown when is canceled or the expires before semaphore access is granted. - /// Thrown when this semaphore is disposed before semaphore access is granted. - public Task EnterAsync(TimeSpan timeout, CancellationToken cancellationToken = default) + WaiterInfo? info = null; + bool shouldCleanupInfo = false; + lock (this.syncObject) { - if (cancellationToken.IsCancellationRequested) + if (this.disposed) { - return Task.FromCanceled(cancellationToken); + return DisposedReleaserTask; } - lock (this.syncObject) + if (this.CurrentCount > 0) + { + this.CurrentCount--; + return this.uncontestedReleaser; + } + else if (timeout == TimeSpan.Zero) { - if (this.disposed) + return CanceledReleaser; + } + else + { + info = new WaiterInfo(this, cancellationToken); + LinkedListNode? node = this.GetNode(info); + + // Careful: consider that if the token was cancelled just now (after we checked it on entry to this method) + // or the timeout expires, + // then this Register method may *inline* the handler we give it, reversing the apparent order of execution with respect to + // the code that follows this Register call. + info.CancellationTokenRegistration = cancellationToken.Register(s => CancellationHandler(s), info); + if (timeout != Timeout.InfiniteTimeSpan) { - return DisposedReleaserTask; + info.TimerTokenSource = new Timer(s => CancellationHandler(s), info, checked((int)timeout.TotalMilliseconds), Timeout.Infinite); } - if (this.CurrentCount > 0) - { - this.CurrentCount--; - return this.uncontestedReleaser; - } - else if (timeout == TimeSpan.Zero) + // Only add to the queue if cancellation hasn't already happened. + if (!info.Trigger.Task.IsCanceled) { - return CanceledReleaser; + this.waiters.AddLast(node); + info.Node = node; } else { - WaiterInfo info = new WaiterInfo(this, cancellationToken); - LinkedListNode? node = this.GetNode(info); - - // Careful: consider that if the token was cancelled just now (after we checked it on entry to this method) - // or the timeout expires, - // then this Register method may *inline* the handler we give it, reversing the apparent order of execution with respect to - // the code that follows this Register call. - info.CancellationTokenRegistration = cancellationToken.Register(s => CancellationHandler(s), info); - if (timeout != Timeout.InfiniteTimeSpan) - { - info.TimerTokenSource = new Timer(s => CancellationHandler(s), info, checked((int)timeout.TotalMilliseconds), Timeout.Infinite); - } - - // Only add to the queue if cancellation hasn't already happened. - if (!info.Trigger.Task.IsCanceled) - { - this.waiters.AddLast(node); - info.Node = node; - } - else - { - // Make sure we don't leak the Timer if cancellation happened before we created it. - info.Cleanup(); - - // Also recycle the unused node. - this.RecycleNode(node); - } + // Make sure we don't leak the Timer if cancellation happened before we created it. + shouldCleanupInfo = true; - return info.Trigger.Task; + // Also recycle the unused node. + this.RecycleNode(node); } } } - /// - /// Requests access to the lock. - /// - /// A timeout for waiting for the lock (in milliseconds). - /// A token whose cancellation signals lost interest in the lock. - /// A task whose result is a releaser that should be disposed to release the lock. - /// Thrown when is canceled or the expires before semaphore access is granted. - /// Thrown when this semaphore is disposed before semaphore access is granted. - public Task EnterAsync(int timeout, CancellationToken cancellationToken = default) => this.EnterAsync(TimeSpan.FromMilliseconds(timeout), cancellationToken); - - /// - /// Faults all pending semaphore waiters with - /// and rejects all subsequent attempts to enter the semaphore with the same exception. - /// - public void Dispose() + // We cleanup outside the lock because cleanup can block on the cancellation handler, + // and the handler can take the same lock as we held earlier in this method. + if (shouldCleanupInfo) { - this.Dispose(true); - GC.SuppressFinalize(this); + info.Cleanup(); } - /// - /// Disposes managed and unmanaged resources held by this instance. - /// - /// true if was called; false if the object is being finalized. - protected virtual void Dispose(bool disposing) + return info.Trigger.Task; + } + + /// + /// Requests access to the lock. + /// + /// A timeout for waiting for the lock (in milliseconds). + /// A token whose cancellation signals lost interest in the lock. + /// A task whose result is a releaser that should be disposed to release the lock. + /// Thrown when is canceled or the expires before semaphore access is granted. + /// Thrown when this semaphore is disposed before semaphore access is granted. + public Task EnterAsync(int timeout, CancellationToken cancellationToken = default) => this.EnterAsync(TimeSpan.FromMilliseconds(timeout), cancellationToken); + + /// + /// Faults all pending semaphore waiters with + /// and rejects all subsequent attempts to enter the semaphore with the same exception. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes managed and unmanaged resources held by this instance. + /// + /// if was called; if the object is being finalized. + protected virtual void Dispose(bool disposing) + { + if (disposing) { - if (disposing) + List? waitersCopy = null; + lock (this.syncObject) { - List? waitersCopy = null; - lock (this.syncObject) - { - this.disposed = true; + this.disposed = true; - if (this.waiters.Count > 0) + if (this.waiters.Count > 0) + { + waitersCopy = new List(this.waiters.Count); + while (this.waiters.First is { } head) { - waitersCopy = new List(this.waiters.Count); - while (this.waiters.First is { } head) - { - head.Value.Trigger.TrySetException(new ObjectDisposedException(this.GetType().FullName)); - waitersCopy.Add(head.Value); - this.waiters.RemoveFirst(); - head.Value.Node = null; - } + head.Value.Trigger.TrySetException(new ObjectDisposedException(this.GetType().FullName)); + waitersCopy.Add(head.Value); + this.waiters.RemoveFirst(); + head.Value.Node = null; } - - this.nodePool.Clear(); } - if (waitersCopy is object) + this.nodePool.Clear(); + } + + if (waitersCopy is object) + { + foreach (WaiterInfo? waitInfo in waitersCopy) { - foreach (WaiterInfo? waitInfo in waitersCopy) - { - waitInfo.Cleanup(); - } + waitInfo.Cleanup(); } } } + } - private static void CancellationHandler(object? state) - { - var waiterInfo = (WaiterInfo)state!; + private static void CancellationHandler(object? state) + { + var waiterInfo = (WaiterInfo)state!; - // The party that manages to complete or cancel the task is responsible to remove it from the queue. - if (waiterInfo.Trigger.TrySetCanceled(waiterInfo.CancellationToken.IsCancellationRequested ? waiterInfo.CancellationToken : new CancellationToken(true))) + // The party that manages to complete or cancel the task is responsible to remove it from the queue. + if (waiterInfo.Trigger.TrySetCanceled(waiterInfo.CancellationToken.IsCancellationRequested ? waiterInfo.CancellationToken : new CancellationToken(true))) + { + // If the node is in the queue, remove it. + // It might not have been added yet if cancellation was already requested by the time we called Register. + lock (waiterInfo.Owner.syncObject) { - // If the node is in the queue, remove it. - // It might not have been added yet if cancellation was already requested by the time we called Register. - lock (waiterInfo.Owner.syncObject) + if (waiterInfo.Node is { } node) { - if (waiterInfo.Node is { } node) - { - waiterInfo.Owner.waiters.Remove(node); - waiterInfo.Owner.RecycleNode(node); - } + waiterInfo.Owner.waiters.Remove(node); + waiterInfo.Owner.RecycleNode(node); } } - - // Clear registration and references. - waiterInfo.Cleanup(); } - private void Release() + // Clear registration and references. + // We *may* be holding a lock on syncObject at this point iff this handler was executed inline with EnterAsync. + // In such a case, we haven't (yet) set WaiterInfo.CTR, so no deadlock risk exists in that case. + waiterInfo.Cleanup(); + } + + private void Release() + { + WaiterInfo? info = null; + lock (this.syncObject) { - WaiterInfo? info = null; - lock (this.syncObject) + if (this.CurrentCount++ == 0) { - if (this.CurrentCount++ == 0) + // We loop because the First node may have been canceled. + while (this.waiters.First is { } head) { - // We loop because the First node may have been canceled. - while (this.waiters.First is { } head) - { - // Remove the head of the queue. - this.waiters.RemoveFirst(); - info = head.Value; - this.RecycleNode(head); + // Remove the head of the queue. + this.waiters.RemoveFirst(); + info = head.Value; + this.RecycleNode(head); - if (info.Trigger.TrySetResult(new Releaser(this))) - { - // We successfully let someone enter the semaphore. - this.CurrentCount--; + if (info.Trigger.TrySetResult(new Releaser(this))) + { + // We successfully let someone enter the semaphore. + this.CurrentCount--; - // We've filled the one slot available in the semaphore. Stop looking for more. - break; - } + // We've filled the one slot available in the semaphore. Stop looking for more. + break; } } } - - // Release memory related to cancellation handling. - info?.Cleanup(); } - private void RecycleNode(LinkedListNode node) + // Release memory related to cancellation handling. + info?.Cleanup(); + } + + private void RecycleNode(LinkedListNode node) + { + Assumes.True(Monitor.IsEntered(this.syncObject)); + node.Value.Node = null; + if (this.nodePool.Count < 10) { - Assumes.True(Monitor.IsEntered(this.syncObject)); - node.Value.Node = null; - if (this.nodePool.Count < 10) - { - LinkedListNode nullableNode = node!; - nullableNode.Value = null; - this.nodePool.Push(nullableNode); - } + LinkedListNode nullableNode = node!; + nullableNode.Value = null; + this.nodePool.Push(nullableNode); } + } - private LinkedListNode GetNode(WaiterInfo info) + private LinkedListNode GetNode(WaiterInfo info) + { + Assumes.True(Monitor.IsEntered(this.syncObject)); + if (this.nodePool.Count > 0) { - Assumes.True(Monitor.IsEntered(this.syncObject)); - if (this.nodePool.Count > 0) - { - LinkedListNode? node = this.nodePool.Pop(); - node.Value = info; - return node!; - } - - return new LinkedListNode(info); + LinkedListNode? node = this.nodePool.Pop(); + node.Value = info; + return node!; } + return new LinkedListNode(info); + } + + /// + /// A value whose disposal triggers the release of a lock. + /// + public readonly struct Releaser : IDisposable + { /// - /// A value whose disposal triggers the release of a lock. + /// The lock instance to release. /// - public readonly struct Releaser : IDisposable + private readonly AsyncSemaphore? toRelease; + + /// + /// Initializes a new instance of the struct. + /// + /// The lock instance to release on. + internal Releaser(AsyncSemaphore toRelease) { - /// - /// The lock instance to release. - /// - private readonly AsyncSemaphore? toRelease; - - /// - /// Initializes a new instance of the struct. - /// - /// The lock instance to release on. - internal Releaser(AsyncSemaphore toRelease) - { - this.toRelease = toRelease; - } + this.toRelease = toRelease; + } - /// - /// Releases the lock. - /// - public void Dispose() + /// + /// Releases the lock. + /// + public void Dispose() + { + if (this.toRelease is object) { - if (this.toRelease is object) - { - this.toRelease.Release(); - } + this.toRelease.Release(); } } + } - private class WaiterInfo + private class WaiterInfo + { + internal WaiterInfo(AsyncSemaphore owner, CancellationToken cancellationToken) { - internal WaiterInfo(AsyncSemaphore owner, CancellationToken cancellationToken) - { - this.Owner = owner; - this.CancellationToken = cancellationToken; - } + this.Owner = owner; + this.CancellationToken = cancellationToken; + } - internal LinkedListNode? Node { get; set; } + internal LinkedListNode? Node { get; set; } - internal AsyncSemaphore Owner { get; } + internal AsyncSemaphore Owner { get; } - internal TaskCompletionSource Trigger { get; } = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + internal TaskCompletionSource Trigger { get; } = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - internal CancellationToken CancellationToken { get; } + internal CancellationToken CancellationToken { get; } - internal CancellationTokenRegistration CancellationTokenRegistration { private get; set; } + internal CancellationTokenRegistration CancellationTokenRegistration { private get; set; } - internal IDisposable? TimerTokenSource { private get; set; } + internal IDisposable? TimerTokenSource { private get; set; } - internal void Cleanup() + /// + /// Disposes of and any applicable timer. + /// + /// + /// Callers should avoid calling this method while holding the lock + /// since can block on completion of + /// which requires that same lock. + /// + internal void Cleanup() + { + CancellationTokenRegistration cancellationTokenRegistration; + IDisposable? timerTokenSource; + lock (this) { - CancellationTokenRegistration cancellationTokenRegistration; - IDisposable? timerTokenSource; - lock (this) - { - cancellationTokenRegistration = this.CancellationTokenRegistration; - this.CancellationTokenRegistration = default; - - timerTokenSource = this.TimerTokenSource; - this.TimerTokenSource = null; - } + cancellationTokenRegistration = this.CancellationTokenRegistration; + this.CancellationTokenRegistration = default; - cancellationTokenRegistration.Dispose(); - timerTokenSource?.Dispose(); + timerTokenSource = this.TimerTokenSource; + this.TimerTokenSource = null; } + + cancellationTokenRegistration.Dispose(); + timerTokenSource?.Dispose(); } } } diff --git a/src/Microsoft.VisualStudio.Threading/AwaitExtensions.cs b/src/Microsoft.VisualStudio.Threading/AwaitExtensions.cs index 2fe9910c3..f1fd44ed8 100644 --- a/src/Microsoft.VisualStudio.Threading/AwaitExtensions.cs +++ b/src/Microsoft.VisualStudio.Threading/AwaitExtensions.cs @@ -1,903 +1,998 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using global::Windows.Win32; +using global::Windows.Win32.Foundation; +using global::Windows.Win32.System.Registry; +using Microsoft.Win32; +using Microsoft.Win32.SafeHandles; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// Extension methods and awaitables for .NET 4.5. +/// +public static partial class AwaitExtensions { - using System; - using System.Collections.Generic; - using System.ComponentModel; - using System.Diagnostics; - using System.Diagnostics.CodeAnalysis; - using System.Runtime.CompilerServices; - using System.Runtime.ExceptionServices; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.Win32; - using Microsoft.Win32.SafeHandles; + /// + /// Gets an awaiter that schedules continuations on the specified scheduler. + /// + /// The task scheduler used to execute continuations. + /// An awaitable. + public static TaskSchedulerAwaiter GetAwaiter(this TaskScheduler scheduler) + { + Requires.NotNull(scheduler, nameof(scheduler)); + return new TaskSchedulerAwaiter(scheduler); + } /// - /// Extension methods and awaitables for .NET 4.5. + /// Gets an awaiter that schedules continuations on the specified . /// - public static partial class AwaitExtensions + /// The synchronization context used to execute continuations. + /// An awaitable. + /// + /// The awaiter that is returned will always result in yielding, even if already executing within the specified . + /// + public static SynchronizationContextAwaiter GetAwaiter(this SynchronizationContext synchronizationContext) { - /// - /// Gets an awaiter that schedules continuations on the specified scheduler. - /// - /// The task scheduler used to execute continuations. - /// An awaitable. - public static TaskSchedulerAwaiter GetAwaiter(this TaskScheduler scheduler) + Requires.NotNull(synchronizationContext, nameof(synchronizationContext)); + return new SynchronizationContextAwaiter(synchronizationContext); + } + + /// + /// Gets an awaitable that schedules continuations on the specified scheduler. + /// + /// The task scheduler used to execute continuations. + /// A value indicating whether the caller should yield even if + /// already executing on the desired task scheduler. + /// An awaitable. + public static TaskSchedulerAwaitable SwitchTo(this TaskScheduler scheduler, bool alwaysYield = false) + { + Requires.NotNull(scheduler, nameof(scheduler)); + return new TaskSchedulerAwaitable(scheduler, alwaysYield); + } + + /// + /// Provides await functionality for ordinary s. + /// + /// The handle to wait on. + /// The awaiter. + public static TaskAwaiter GetAwaiter(this WaitHandle handle) + { + Requires.NotNull(handle, nameof(handle)); + Task task = handle.ToTask(); + return task.GetAwaiter(); + } + + /// + /// Returns a task that completes when the process exits and provides the exit code of that process. + /// + /// The process to wait for exit. + /// + /// A token whose cancellation will cause the returned Task to complete + /// before the process exits in a faulted state with an . + /// This token has no effect on the itself. + /// + /// A task whose result is the of the . + public static async Task WaitForExitAsync(this Process process, CancellationToken cancellationToken = default(CancellationToken)) + { + Requires.NotNull(process, nameof(process)); + + var tcs = new TaskCompletionSource(); + EventHandler exitHandler = (s, e) => + { + tcs.TrySetResult(process.ExitCode); + }; + try + { + process.EnableRaisingEvents = true; + process.Exited += exitHandler; + if (process.HasExited) + { + // Allow for the race condition that the process has already exited. + tcs.TrySetResult(process.ExitCode); + } + + using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken))) + { + return await tcs.Task.ConfigureAwait(false); + } + } + finally { - Requires.NotNull(scheduler, nameof(scheduler)); - return new TaskSchedulerAwaiter(scheduler); + process.Exited -= exitHandler; } + } - /// - /// Gets an awaitable that schedules continuations on the specified scheduler. - /// - /// The task scheduler used to execute continuations. - /// A value indicating whether the caller should yield even if - /// already executing on the desired task scheduler. - /// An awaitable. - public static TaskSchedulerAwaitable SwitchTo(this TaskScheduler scheduler, bool alwaysYield = false) + /// + /// Returns a Task that completes when the specified registry key changes. + /// + /// The registry key to watch for changes. + /// to watch the keys descendent keys as well; to watch only this key without descendents. + /// Indicates the kinds of changes to watch for. + /// A token that may be canceled to release the resources from watching for changes and complete the returned Task as canceled. + /// + /// A task that completes when the registry key changes, the handle is closed, or upon cancellation. + /// + public static Task WaitForChangeAsync(this RegistryKey registryKey, bool watchSubtree = true, RegistryChangeNotificationFilters change = RegistryChangeNotificationFilters.Value | RegistryChangeNotificationFilters.Subkey, CancellationToken cancellationToken = default(CancellationToken)) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - Requires.NotNull(scheduler, nameof(scheduler)); - return new TaskSchedulerAwaitable(scheduler, alwaysYield); + throw new PlatformNotSupportedException(); } - /// - /// Provides await functionality for ordinary s. - /// - /// The handle to wait on. - /// The awaiter. - public static TaskAwaiter GetAwaiter(this WaitHandle handle) + Requires.NotNull(registryKey, nameof(registryKey)); + + return WaitForRegistryChangeAsync(registryKey.Handle, watchSubtree, change, cancellationToken); + } + + /// + /// Converts a to a . + /// + /// The result of . + /// A value indicating whether the continuation should run on the captured , if any. + /// An awaitable. + public static ConfiguredTaskYieldAwaitable ConfigureAwait(this YieldAwaitable yieldAwaitable, bool continueOnCapturedContext) + { + return new ConfiguredTaskYieldAwaitable(continueOnCapturedContext); + } + + /// + /// Gets an awaitable that schedules the continuation with a preference to executing synchronously on the callstack that completed the , + /// without regard to thread ID or any that may be applied when the continuation is scheduled or when the antecedent completes. + /// + /// The task to await on. + /// An awaitable. + /// + /// If there is not enough stack space remaining on the thread that is completing the , + /// the continuation may be scheduled on the threadpool. + /// + public static ExecuteContinuationSynchronouslyAwaitable ConfigureAwaitRunInline(this Task antecedent) + { + Requires.NotNull(antecedent, nameof(antecedent)); + + return new ExecuteContinuationSynchronouslyAwaitable(antecedent); + } + + /// + /// Gets an awaitable that schedules the continuation with a preference to executing synchronously on the callstack that completed the , + /// without regard to thread ID or any that may be applied when the continuation is scheduled or when the antecedent completes. + /// + /// The type of value returned by the awaited . + /// The task to await on. + /// An awaitable. + /// + /// If there is not enough stack space remaining on the thread that is completing the , + /// the continuation may be scheduled on the threadpool. + /// + public static ExecuteContinuationSynchronouslyAwaitable ConfigureAwaitRunInline(this Task antecedent) + { + Requires.NotNull(antecedent, nameof(antecedent)); + + return new ExecuteContinuationSynchronouslyAwaitable(antecedent); + } + + /// + /// Returns an awaitable that will throw from the property of the task if it faults. + /// + /// The task to track for completion. + /// + /// An awaitable that may throw . + /// + /// Awaiting a with its default only throws the first exception within . + /// When you do not want to lose the detail of other inner exceptions, use this extension method. + /// + /// Thrown when faults. + public static AggregateExceptionAwaitable ConfigureAwaitForAggregateException(this Task task, bool continueOnCapturedContext = true) => new AggregateExceptionAwaitable(task, continueOnCapturedContext); + + /// + /// Returns a Task that completes when the specified registry key changes. + /// + /// The handle to the open registry key to watch for changes. + /// to watch the keys descendent keys as well; to watch only this key without descendents. + /// Indicates the kinds of changes to watch for. + /// A token that may be canceled to release the resources from watching for changes and complete the returned Task as canceled. + /// + /// A task that completes when the registry key changes, the handle is closed, or upon cancellation. + /// + private static async Task WaitForRegistryChangeAsync(SafeRegistryHandle registryKeyHandle, bool watchSubtree, RegistryChangeNotificationFilters change, CancellationToken cancellationToken) + { +#if NET5_0_OR_GREATER + if (!OperatingSystem.IsWindowsVersionAtLeast(7)) +#else + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) +#endif { - Requires.NotNull(handle, nameof(handle)); - Task task = handle.ToTask(); - return task.GetAwaiter(); + throw new PlatformNotSupportedException(); } - /// - /// Returns a task that completes when the process exits and provides the exit code of that process. - /// - /// The process to wait for exit. - /// - /// A token whose cancellation will cause the returned Task to complete - /// before the process exits in a faulted state with an . - /// This token has no effect on the itself. - /// - /// A task whose result is the of the . - public static async Task WaitForExitAsync(this Process process, CancellationToken cancellationToken = default(CancellationToken)) + IDisposable? dedicatedThreadReleaser = null; + try { - Requires.NotNull(process, nameof(process)); + using ManualResetEvent evt = new(false); + REG_NOTIFY_FILTER dwNotifyFilter = (REG_NOTIFY_FILTER)change; - var tcs = new TaskCompletionSource(); - EventHandler exitHandler = (s, e) => - { - tcs.TrySetResult(process.ExitCode); - }; - try + static void DoNotify(SafeRegistryHandle registryKeyHandle, bool watchSubtree, REG_NOTIFY_FILTER change, WaitHandle evt) { - process.EnableRaisingEvents = true; - process.Exited += exitHandler; - if (process.HasExited) + WIN32_ERROR win32Error = PInvoke.RegNotifyChangeKeyValue( + registryKeyHandle, + watchSubtree, + change, + evt.SafeWaitHandle, + true); + if (win32Error != 0) { - // Allow for the race condition that the process has already exited. - tcs.TrySetResult(process.ExitCode); + throw new Win32Exception((int)win32Error); } + } - using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken))) - { - return await tcs.Task.ConfigureAwait(false); - } + if (LightUps.IsWindows8OrLater) + { + dwNotifyFilter |= REG_NOTIFY_FILTER.REG_NOTIFY_THREAD_AGNOSTIC; + DoNotify(registryKeyHandle, watchSubtree, dwNotifyFilter, evt); } - finally + else { - process.Exited -= exitHandler; + // Engage our downlevel support by using a single, dedicated thread to guarantee + // that we request notification on a thread that will not be destroyed later. + // Although we *could* await this, we synchronously block because our caller expects + // subscription to have begun before we return: for the async part to simply be notification. + // This async method we're calling uses .ConfigureAwait(false) internally so this won't + // deadlock if we're called on a thread with a single-thread SynchronizationContext. + Action registerAction = () => DoNotify(registryKeyHandle, watchSubtree, dwNotifyFilter, evt); + dedicatedThreadReleaser = DownlevelRegistryWatcherSupport.ExecuteOnDedicatedThreadAsync(registerAction).GetAwaiter().GetResult(); } + + await evt.ToTask(cancellationToken: cancellationToken).ConfigureAwait(false); + } + finally + { + dedicatedThreadReleaser?.Dispose(); } + } + + /// + /// The result of to prepare a to be awaited while throwing with all inner exceptions. + /// + public readonly struct AggregateExceptionAwaitable + { + private readonly Task task; + private readonly bool continueOnCapturedContext; /// - /// Returns a Task that completes when the specified registry key changes. + /// Initializes a new instance of the struct. /// - /// The registry key to watch for changes. - /// true to watch the keys descendent keys as well; false to watch only this key without descendents. - /// Indicates the kinds of changes to watch for. - /// A token that may be canceled to release the resources from watching for changes and complete the returned Task as canceled. - /// - /// A task that completes when the registry key changes, the handle is closed, or upon cancellation. - /// - public static Task WaitForChangeAsync(this RegistryKey registryKey, bool watchSubtree = true, RegistryChangeNotificationFilters change = RegistryChangeNotificationFilters.Value | RegistryChangeNotificationFilters.Subkey, CancellationToken cancellationToken = default(CancellationToken)) + public AggregateExceptionAwaitable(Task task, bool continueOnCapturedContext) { - Requires.NotNull(registryKey, nameof(registryKey)); - - return WaitForRegistryChangeAsync(registryKey.Handle, watchSubtree, change, cancellationToken); + this.task = task; + this.continueOnCapturedContext = continueOnCapturedContext; } /// - /// Converts a to a . + /// Gets an awaitable that schedules continuations on the specified scheduler. /// - /// The result of . - /// A value indicating whether the continuation should run on the captured , if any. - /// An awaitable. - public static ConfiguredTaskYieldAwaitable ConfigureAwait(this YieldAwaitable yieldAwaitable, bool continueOnCapturedContext) + public AggregateExceptionAwaiter GetAwaiter() { - return new ConfiguredTaskYieldAwaitable(continueOnCapturedContext); + return new AggregateExceptionAwaiter(this.task, this.continueOnCapturedContext); } + } + + /// + /// The result of to prepare a to be awaited while throwing with all inner exceptions. + /// + public readonly struct AggregateExceptionAwaiter : ICriticalNotifyCompletion + { + private readonly Task task; + private readonly bool continueOnCapturedContext; /// - /// Gets an awaitable that schedules the continuation with a preference to executing synchronously on the callstack that completed the , - /// without regard to thread ID or any that may be applied when the continuation is scheduled or when the antecedent completes. + /// Initializes a new instance of the struct. /// - /// The task to await on. - /// An awaitable. - /// - /// If there is not enough stack space remaining on the thread that is completing the , - /// the continuation may be scheduled on the threadpool. - /// - public static ExecuteContinuationSynchronouslyAwaitable ConfigureAwaitRunInline(this Task antecedent) + public AggregateExceptionAwaiter(Task task, bool continueOnCapturedContext) { - Requires.NotNull(antecedent, nameof(antecedent)); - - return new ExecuteContinuationSynchronouslyAwaitable(antecedent); + this.task = task; + this.continueOnCapturedContext = continueOnCapturedContext; } - /// - /// Gets an awaitable that schedules the continuation with a preference to executing synchronously on the callstack that completed the , - /// without regard to thread ID or any that may be applied when the continuation is scheduled or when the antecedent completes. - /// - /// The type of value returned by the awaited . - /// The task to await on. - /// An awaitable. - /// - /// If there is not enough stack space remaining on the thread that is completing the , - /// the continuation may be scheduled on the threadpool. - /// - public static ExecuteContinuationSynchronouslyAwaitable ConfigureAwaitRunInline(this Task antecedent) + /// + public bool IsCompleted => this.Awaiter.IsCompleted; + + private ConfiguredTaskAwaitable.ConfiguredTaskAwaiter Awaiter => this.task.ConfigureAwait(this.continueOnCapturedContext).GetAwaiter(); + + /// + public void OnCompleted(Action continuation) => this.Awaiter.OnCompleted(continuation); + + /// + public void UnsafeOnCompleted(Action continuation) => this.Awaiter.UnsafeOnCompleted(continuation); + + /// + /// Thrown if the task was canceled. + /// Thrown if the task faulted. + public void GetResult() { - Requires.NotNull(antecedent, nameof(antecedent)); + if (this.task.Status == TaskStatus.Faulted && this.task.Exception is object) + { + ExceptionDispatchInfo.Capture(this.task.Exception).Throw(); + } - return new ExecuteContinuationSynchronouslyAwaitable(antecedent); + this.Awaiter.GetResult(); } + } + /// + /// An awaitable that executes continuations on the specified task scheduler. + /// + public readonly struct TaskSchedulerAwaitable + { /// - /// Returns an awaitable that will throw from the property of the task if it faults. + /// The scheduler for continuations. /// - /// The task to track for completion. - /// - /// An awaitable that may throw . - /// - /// Awaiting a with its default only throws the first exception within . - /// When you do not want to lose the detail of other inner exceptions, use this extension method. - /// - /// Thrown when faults. - public static AggregateExceptionAwaitable ConfigureAwaitForAggregateException(this Task task, bool continueOnCapturedContext = true) => new AggregateExceptionAwaitable(task, continueOnCapturedContext); + private readonly TaskScheduler taskScheduler; /// - /// Returns a Task that completes when the specified registry key changes. + /// A value indicating whether the awaitable will always call the caller to yield. /// - /// The handle to the open registry key to watch for changes. - /// true to watch the keys descendent keys as well; false to watch only this key without descendents. - /// Indicates the kinds of changes to watch for. - /// A token that may be canceled to release the resources from watching for changes and complete the returned Task as canceled. - /// - /// A task that completes when the registry key changes, the handle is closed, or upon cancellation. - /// - private static async Task WaitForRegistryChangeAsync(SafeRegistryHandle registryKeyHandle, bool watchSubtree, RegistryChangeNotificationFilters change, CancellationToken cancellationToken) - { - IDisposable? dedicatedThreadReleaser = null; - try - { - using (var evt = new ManualResetEvent(false)) - { - static void DoNotify(SafeRegistryHandle registryKeyHandle, bool watchSubtree, RegistryChangeNotificationFilters change, WaitHandle evt) - { - int win32Error = NativeMethods.RegNotifyChangeKeyValue( - registryKeyHandle, - watchSubtree, - change, - evt.SafeWaitHandle, - true); - if (win32Error != 0) - { - throw new Win32Exception(win32Error); - } - } - - if (LightUps.IsWindows8OrLater) - { - change |= NativeMethods.REG_NOTIFY_THREAD_AGNOSTIC; - DoNotify(registryKeyHandle, watchSubtree, change, evt); - } - else - { - // Engage our downlevel support by using a single, dedicated thread to guarantee - // that we request notification on a thread that will not be destroyed later. - // Although we *could* await this, we synchronously block because our caller expects - // subscription to have begun before we return: for the async part to simply be notification. - // This async method we're calling uses .ConfigureAwait(false) internally so this won't - // deadlock if we're called on a thread with a single-thread SynchronizationContext. - Action registerAction = () => DoNotify(registryKeyHandle, watchSubtree, change, evt); - dedicatedThreadReleaser = DownlevelRegistryWatcherSupport.ExecuteOnDedicatedThreadAsync(registerAction).GetAwaiter().GetResult(); - } - - await evt.ToTask(cancellationToken: cancellationToken).ConfigureAwait(false); - } - } - finally - { - dedicatedThreadReleaser?.Dispose(); - } - } + private readonly bool alwaysYield; /// - /// The result of to prepare a to be awaited while throwing with all inner exceptions. + /// Initializes a new instance of the struct. /// - public readonly struct AggregateExceptionAwaitable + /// The task scheduler used to execute continuations. + /// A value indicating whether the caller should yield even if + /// already executing on the desired task scheduler. + public TaskSchedulerAwaitable(TaskScheduler taskScheduler, bool alwaysYield = false) { - private readonly Task task; - private readonly bool continueOnCapturedContext; - - /// - /// Initializes a new instance of the struct. - /// - public AggregateExceptionAwaitable(Task task, bool continueOnCapturedContext) - { - this.task = task; - this.continueOnCapturedContext = continueOnCapturedContext; - } + Requires.NotNull(taskScheduler, nameof(taskScheduler)); - /// - /// Gets an awaitable that schedules continuations on the specified scheduler. - /// - public AggregateExceptionAwaiter GetAwaiter() - { - return new AggregateExceptionAwaiter(this.task, this.continueOnCapturedContext); - } + this.taskScheduler = taskScheduler; + this.alwaysYield = alwaysYield; } /// - /// The result of to prepare a to be awaited while throwing with all inner exceptions. + /// Gets an awaitable that schedules continuations on the specified scheduler. /// - public readonly struct AggregateExceptionAwaiter : ICriticalNotifyCompletion + public TaskSchedulerAwaiter GetAwaiter() { - private readonly Task task; - private readonly bool continueOnCapturedContext; - - /// - /// Initializes a new instance of the struct. - /// - public AggregateExceptionAwaiter(Task task, bool continueOnCapturedContext) - { - this.task = task; - this.continueOnCapturedContext = continueOnCapturedContext; - } - - /// - public bool IsCompleted => this.Awaiter.IsCompleted; + return new TaskSchedulerAwaiter(this.taskScheduler, this.alwaysYield); + } + } - private ConfiguredTaskAwaitable.ConfiguredTaskAwaiter Awaiter => this.task.ConfigureAwait(this.continueOnCapturedContext).GetAwaiter(); + /// + /// An awaiter returned from . + /// + public readonly struct TaskSchedulerAwaiter : ICriticalNotifyCompletion + { + /// + /// The scheduler for continuations. + /// + private readonly TaskScheduler scheduler; - /// - public void OnCompleted(Action continuation) => this.Awaiter.OnCompleted(continuation); + /// + /// A value indicating whether + /// should always return false. + /// + private readonly bool alwaysYield; - /// - public void UnsafeOnCompleted(Action continuation) => this.Awaiter.UnsafeOnCompleted(continuation); + /// + /// Initializes a new instance of the struct. + /// + /// The scheduler for continuations. + /// A value indicating whether the caller should yield even if + /// already executing on the desired task scheduler. + public TaskSchedulerAwaiter(TaskScheduler scheduler, bool alwaysYield = false) + { + this.scheduler = scheduler; + this.alwaysYield = alwaysYield; + } - /// - /// Thrown if the task was canceled. - /// Thrown if the task faulted. - public void GetResult() + /// + /// Gets a value indicating whether no yield is necessary. + /// + /// if the caller is already running on that TaskScheduler. + public bool IsCompleted + { + get { - if (this.task.Status == TaskStatus.Faulted && this.task.Exception is object) + if (this.alwaysYield) { - ExceptionDispatchInfo.Capture(this.task.Exception).Throw(); + return false; } - this.Awaiter.GetResult(); + // We special case the TaskScheduler.Default since that is semantically equivalent to being + // on a ThreadPool thread, and there are various ways to get on those threads. + // TaskScheduler.Current is never null. Even if no scheduler is really active and the current + // thread is not a threadpool thread, TaskScheduler.Current == TaskScheduler.Default, so we have + // to protect against that case too. + bool isThreadPoolThread = Thread.CurrentThread.IsThreadPoolThread; + return (this.scheduler == TaskScheduler.Default && isThreadPoolThread) + || (this.scheduler == TaskScheduler.Current && TaskScheduler.Current != TaskScheduler.Default); } } /// - /// An awaitable that executes continuations on the specified task scheduler. + /// Schedules a continuation to execute using the specified task scheduler. /// - public readonly struct TaskSchedulerAwaitable + /// The delegate to invoke. + public void OnCompleted(Action continuation) { - /// - /// The scheduler for continuations. - /// - private readonly TaskScheduler taskScheduler; - - /// - /// A value indicating whether the awaitable will always call the caller to yield. - /// - private readonly bool alwaysYield; - - /// - /// Initializes a new instance of the struct. - /// - /// The task scheduler used to execute continuations. - /// A value indicating whether the caller should yield even if - /// already executing on the desired task scheduler. - public TaskSchedulerAwaitable(TaskScheduler taskScheduler, bool alwaysYield = false) + if (this.scheduler == TaskScheduler.Default) { - Requires.NotNull(taskScheduler, nameof(taskScheduler)); - - this.taskScheduler = taskScheduler; - this.alwaysYield = alwaysYield; + ThreadPool.QueueUserWorkItem(state => ((Action)state!)(), continuation); } - - /// - /// Gets an awaitable that schedules continuations on the specified scheduler. - /// - public TaskSchedulerAwaiter GetAwaiter() + else { - return new TaskSchedulerAwaiter(this.taskScheduler, this.alwaysYield); + Task.Factory.StartNew(continuation, CancellationToken.None, TaskCreationOptions.None, this.scheduler); } } /// - /// An awaiter returned from . + /// Schedules a continuation to execute using the specified task scheduler + /// without capturing the ExecutionContext. /// - public readonly struct TaskSchedulerAwaiter : ICriticalNotifyCompletion + /// The action. + public void UnsafeOnCompleted(Action continuation) { - /// - /// The scheduler for continuations. - /// - private readonly TaskScheduler scheduler; - - /// - /// A value indicating whether - /// should always return false. - /// - private readonly bool alwaysYield; - - /// - /// Initializes a new instance of the struct. - /// - /// The scheduler for continuations. - /// A value indicating whether the caller should yield even if - /// already executing on the desired task scheduler. - public TaskSchedulerAwaiter(TaskScheduler scheduler, bool alwaysYield = false) - { - this.scheduler = scheduler; - this.alwaysYield = alwaysYield; - } - - /// - /// Gets a value indicating whether no yield is necessary. - /// - /// true if the caller is already running on that TaskScheduler. - public bool IsCompleted + if (this.scheduler == TaskScheduler.Default) { - get - { - if (this.alwaysYield) - { - return false; - } - - // We special case the TaskScheduler.Default since that is semantically equivalent to being - // on a ThreadPool thread, and there are various ways to get on those threads. - // TaskScheduler.Current is never null. Even if no scheduler is really active and the current - // thread is not a threadpool thread, TaskScheduler.Current == TaskScheduler.Default, so we have - // to protect against that case too. - bool isThreadPoolThread = Thread.CurrentThread.IsThreadPoolThread; - return (this.scheduler == TaskScheduler.Default && isThreadPoolThread) - || (this.scheduler == TaskScheduler.Current && TaskScheduler.Current != TaskScheduler.Default); - } + ThreadPool.UnsafeQueueUserWorkItem(state => ((Action)state!)(), continuation); } - - /// - /// Schedules a continuation to execute using the specified task scheduler. - /// - /// The delegate to invoke. - public void OnCompleted(Action continuation) + else { - if (this.scheduler == TaskScheduler.Default) - { - ThreadPool.QueueUserWorkItem(state => ((Action)state!)(), continuation); - } - else +#if NETFRAMEWORK // Only bother suppressing flow on .NET Framework where the perf would improve from doing so. + if (ExecutionContext.IsFlowSuppressed()) { Task.Factory.StartNew(continuation, CancellationToken.None, TaskCreationOptions.None, this.scheduler); } - } - - /// - /// Schedules a continuation to execute using the specified task scheduler - /// without capturing the ExecutionContext. - /// - /// The action. - public void UnsafeOnCompleted(Action continuation) - { - if (this.scheduler == TaskScheduler.Default) - { - ThreadPool.UnsafeQueueUserWorkItem(state => ((Action)state!)(), continuation); - } else { -#if NETFRAMEWORK // Only bother suppressing flow on .NET Framework where the perf would improve from doing so. - if (ExecutionContext.IsFlowSuppressed()) + using (ExecutionContext.SuppressFlow()) { Task.Factory.StartNew(continuation, CancellationToken.None, TaskCreationOptions.None, this.scheduler); } - else - { - using (ExecutionContext.SuppressFlow()) - { - Task.Factory.StartNew(continuation, CancellationToken.None, TaskCreationOptions.None, this.scheduler); - } - } + } #else - Task.Factory.StartNew(continuation, CancellationToken.None, TaskCreationOptions.None, this.scheduler); + Task.Factory.StartNew(continuation, CancellationToken.None, TaskCreationOptions.None, this.scheduler); #endif - } - } - - /// - /// Does nothing. - /// - public void GetResult() - { } } /// - /// An awaitable that will always lead the calling async method to yield, - /// then immediately resume, possibly on the original . + /// Does nothing. /// - public readonly struct ConfiguredTaskYieldAwaitable + public void GetResult() { - /// - /// A value indicating whether the continuation should run on the captured , if any. - /// - private readonly bool continueOnCapturedContext; + } + } - /// - /// Initializes a new instance of the struct. - /// - /// A value indicating whether the continuation should run on the captured , if any. - public ConfiguredTaskYieldAwaitable(bool continueOnCapturedContext) - { - this.continueOnCapturedContext = continueOnCapturedContext; - } + /// + /// An awaiter returned from . + /// + public readonly struct SynchronizationContextAwaiter : ICriticalNotifyCompletion + { + private static readonly SendOrPostCallback SyncContextDelegate = s => ((Action)s!)(); - /// - /// Gets the awaiter. - /// - /// The awaiter. - public ConfiguredTaskYieldAwaiter GetAwaiter() => new ConfiguredTaskYieldAwaiter(this.continueOnCapturedContext); - } + /// + /// The context for continuations. + /// + private readonly SynchronizationContext syncContext; /// - /// An awaiter that will always lead the calling async method to yield, - /// then immediately resume, possibly on the original . + /// Initializes a new instance of the struct. /// - public readonly struct ConfiguredTaskYieldAwaiter : ICriticalNotifyCompletion + /// The context for continuations. + public SynchronizationContextAwaiter(SynchronizationContext syncContext) { - /// - /// A value indicating whether the continuation should run on the captured , if any. - /// - private readonly bool continueOnCapturedContext; + this.syncContext = syncContext; + } - /// - /// Initializes a new instance of the struct. - /// - /// A value indicating whether the continuation should run on the captured , if any. - public ConfiguredTaskYieldAwaiter(bool continueOnCapturedContext) - { - this.continueOnCapturedContext = continueOnCapturedContext; - } + /// + /// Gets a value indicating whether no yield is necessary. + /// + /// Always returns . + public bool IsCompleted => false; - /// - /// Gets a value indicating whether the caller should yield. - /// - /// Always false. - public bool IsCompleted => false; + /// + /// Schedules a continuation to execute using the specified . + /// + /// The delegate to invoke. + public void OnCompleted(Action continuation) => this.syncContext.Post(SyncContextDelegate, continuation); - /// - /// Schedules a continuation to execute immediately (but not synchronously). - /// - /// The delegate to invoke. - public void OnCompleted(Action continuation) + /// + /// Schedules a continuation to execute using the specified + /// without capturing the . + /// + /// The action. + public void UnsafeOnCompleted(Action continuation) + { +#if NETFRAMEWORK // Only bother suppressing flow on .NET Framework where the perf would improve from doing so. + if (ExecutionContext.IsFlowSuppressed()) { - if (this.continueOnCapturedContext) - { - Task.Yield().GetAwaiter().OnCompleted(continuation); - } - else - { - ThreadPool.QueueUserWorkItem(state => ((Action)state!)(), continuation); - } + this.syncContext.Post(SyncContextDelegate, continuation); } - - /// - /// Schedules a delegate for execution at the conclusion of a task's execution - /// without capturing the ExecutionContext. - /// - /// The action. - public void UnsafeOnCompleted(Action continuation) + else { - if (this.continueOnCapturedContext) - { - Task.Yield().GetAwaiter().UnsafeOnCompleted(continuation); - } - else + using (ExecutionContext.SuppressFlow()) { - ThreadPool.UnsafeQueueUserWorkItem(state => ((Action)state!)(), continuation); + this.syncContext.Post(SyncContextDelegate, continuation); } } +#else + this.syncContext.Post(SyncContextDelegate, continuation); +#endif + } - /// - /// Does nothing. - /// - public void GetResult() - { - } + /// + /// Does nothing. + /// + public void GetResult() + { } + } + + /// + /// An awaitable that will always lead the calling async method to yield, + /// then immediately resume, possibly on the original . + /// + public readonly struct ConfiguredTaskYieldAwaitable + { + /// + /// A value indicating whether the continuation should run on the captured , if any. + /// + private readonly bool continueOnCapturedContext; /// - /// A Task awaitable that has affinity to executing callbacks synchronously on the completing callstack. + /// Initializes a new instance of the struct. /// - public readonly struct ExecuteContinuationSynchronouslyAwaitable + /// A value indicating whether the continuation should run on the captured , if any. + public ConfiguredTaskYieldAwaitable(bool continueOnCapturedContext) { - /// - /// The task whose completion will execute the continuation. - /// - private readonly Task antecedent; + this.continueOnCapturedContext = continueOnCapturedContext; + } - /// - /// Initializes a new instance of the struct. - /// - /// The task whose completion will execute the continuation. - public ExecuteContinuationSynchronouslyAwaitable(Task antecedent) - { - Requires.NotNull(antecedent, nameof(antecedent)); - this.antecedent = antecedent; - } + /// + /// Gets the awaiter. + /// + /// The awaiter. + public ConfiguredTaskYieldAwaiter GetAwaiter() => new ConfiguredTaskYieldAwaiter(this.continueOnCapturedContext); + } - /// - /// Gets the awaiter. - /// - /// The awaiter. - public ExecuteContinuationSynchronouslyAwaiter GetAwaiter() => new ExecuteContinuationSynchronouslyAwaiter(this.antecedent); + /// + /// An awaiter that will always lead the calling async method to yield, + /// then immediately resume, possibly on the original . + /// + public readonly struct ConfiguredTaskYieldAwaiter : ICriticalNotifyCompletion + { + /// + /// A value indicating whether the continuation should run on the captured , if any. + /// + private readonly bool continueOnCapturedContext; + + /// + /// Initializes a new instance of the struct. + /// + /// A value indicating whether the continuation should run on the captured , if any. + public ConfiguredTaskYieldAwaiter(bool continueOnCapturedContext) + { + this.continueOnCapturedContext = continueOnCapturedContext; } /// - /// A Task awaiter that has affinity to executing callbacks synchronously on the completing callstack. + /// Gets a value indicating whether the caller should yield. + /// + /// Always false. + public bool IsCompleted => false; + + /// + /// Schedules a continuation to execute immediately (but not synchronously). /// - public readonly struct ExecuteContinuationSynchronouslyAwaiter : INotifyCompletion + /// The delegate to invoke. + public void OnCompleted(Action continuation) { - /// - /// The task whose completion will execute the continuation. - /// - private readonly Task antecedent; + if (this.continueOnCapturedContext) + { + Task.Yield().GetAwaiter().OnCompleted(continuation); + } + else + { + ThreadPool.QueueUserWorkItem(state => ((Action)state!)(), continuation); + } + } - /// - /// Initializes a new instance of the struct. - /// - /// The task whose completion will execute the continuation. - public ExecuteContinuationSynchronouslyAwaiter(Task antecedent) + /// + /// Schedules a delegate for execution at the conclusion of a task's execution + /// without capturing the ExecutionContext. + /// + /// The action. + public void UnsafeOnCompleted(Action continuation) + { + if (this.continueOnCapturedContext) { - Requires.NotNull(antecedent, nameof(antecedent)); - this.antecedent = antecedent; + Task.Yield().GetAwaiter().UnsafeOnCompleted(continuation); } + else + { + ThreadPool.UnsafeQueueUserWorkItem(state => ((Action)state!)(), continuation); + } + } - /// - /// Gets a value indicating whether the antedent has already completed. - /// - public bool IsCompleted => this.antecedent.IsCompleted; + /// + /// Does nothing. + /// + public void GetResult() + { + } + } - /// - /// Rethrows any exception thrown by the antecedent. - /// - public void GetResult() => this.antecedent.GetAwaiter().GetResult(); + /// + /// A Task awaitable that has affinity to executing callbacks synchronously on the completing callstack. + /// + public readonly struct ExecuteContinuationSynchronouslyAwaitable + { + /// + /// The task whose completion will execute the continuation. + /// + private readonly Task antecedent; - /// - /// Schedules a callback to run when the antecedent task completes. - /// - /// The callback to invoke. - public void OnCompleted(Action continuation) - { - Requires.NotNull(continuation, nameof(continuation)); - - this.antecedent.ContinueWith( - (_, s) => ((Action)s!)(), - continuation, - CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default); - } + /// + /// Initializes a new instance of the struct. + /// + /// The task whose completion will execute the continuation. + public ExecuteContinuationSynchronouslyAwaitable(Task antecedent) + { + Requires.NotNull(antecedent, nameof(antecedent)); + this.antecedent = antecedent; } /// - /// A Task awaitable that has affinity to executing callbacks synchronously on the completing callstack. + /// Gets the awaiter. + /// + /// The awaiter. + public ExecuteContinuationSynchronouslyAwaiter GetAwaiter() => new ExecuteContinuationSynchronouslyAwaiter(this.antecedent); + } + + /// + /// A Task awaiter that has affinity to executing callbacks synchronously on the completing callstack. + /// + public readonly struct ExecuteContinuationSynchronouslyAwaiter : INotifyCompletion + { + /// + /// The task whose completion will execute the continuation. + /// + private readonly Task antecedent; + + /// + /// Initializes a new instance of the struct. /// - /// The type of value returned by the awaited . - public readonly struct ExecuteContinuationSynchronouslyAwaitable + /// The task whose completion will execute the continuation. + public ExecuteContinuationSynchronouslyAwaiter(Task antecedent) { - /// - /// The task whose completion will execute the continuation. - /// - private readonly Task antecedent; + Requires.NotNull(antecedent, nameof(antecedent)); + this.antecedent = antecedent; + } - /// - /// Initializes a new instance of the struct. - /// - /// The task whose completion will execute the continuation. - public ExecuteContinuationSynchronouslyAwaitable(Task antecedent) - { - Requires.NotNull(antecedent, nameof(antecedent)); - this.antecedent = antecedent; - } + /// + /// Gets a value indicating whether the antedent has already completed. + /// + public bool IsCompleted => this.antecedent.IsCompleted; - /// - /// Gets the awaiter. - /// - /// The awaiter. - public ExecuteContinuationSynchronouslyAwaiter GetAwaiter() => new ExecuteContinuationSynchronouslyAwaiter(this.antecedent); + /// + /// Rethrows any exception thrown by the antecedent. + /// + public void GetResult() => this.antecedent.GetAwaiter().GetResult(); + + /// + /// Schedules a callback to run when the antecedent task completes. + /// + /// The callback to invoke. + public void OnCompleted(Action continuation) + { + Requires.NotNull(continuation, nameof(continuation)); + + this.antecedent.ContinueWith( + (_, s) => ((Action)s!)(), + continuation, + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); } + } + + /// + /// A Task awaitable that has affinity to executing callbacks synchronously on the completing callstack. + /// + /// The type of value returned by the awaited . + public readonly struct ExecuteContinuationSynchronouslyAwaitable + { + /// + /// The task whose completion will execute the continuation. + /// + private readonly Task antecedent; /// - /// A Task awaiter that has affinity to executing callbacks synchronously on the completing callstack. + /// Initializes a new instance of the struct. /// - /// The type of value returned by the awaited . - public readonly struct ExecuteContinuationSynchronouslyAwaiter : INotifyCompletion + /// The task whose completion will execute the continuation. + public ExecuteContinuationSynchronouslyAwaitable(Task antecedent) { - /// - /// The task whose completion will execute the continuation. - /// - private readonly Task antecedent; + Requires.NotNull(antecedent, nameof(antecedent)); + this.antecedent = antecedent; + } - /// - /// Initializes a new instance of the struct. - /// - /// The task whose completion will execute the continuation. - public ExecuteContinuationSynchronouslyAwaiter(Task antecedent) - { - Requires.NotNull(antecedent, nameof(antecedent)); - this.antecedent = antecedent; - } + /// + /// Gets the awaiter. + /// + /// The awaiter. + public ExecuteContinuationSynchronouslyAwaiter GetAwaiter() => new ExecuteContinuationSynchronouslyAwaiter(this.antecedent); + } - /// - /// Gets a value indicating whether the antedent has already completed. - /// - public bool IsCompleted => this.antecedent.IsCompleted; + /// + /// A Task awaiter that has affinity to executing callbacks synchronously on the completing callstack. + /// + /// The type of value returned by the awaited . + public readonly struct ExecuteContinuationSynchronouslyAwaiter : INotifyCompletion + { + /// + /// The task whose completion will execute the continuation. + /// + private readonly Task antecedent; - /// - /// Rethrows any exception thrown by the antecedent. - /// - public T GetResult() => this.antecedent.GetAwaiter().GetResult(); + /// + /// Initializes a new instance of the struct. + /// + /// The task whose completion will execute the continuation. + public ExecuteContinuationSynchronouslyAwaiter(Task antecedent) + { + Requires.NotNull(antecedent, nameof(antecedent)); + this.antecedent = antecedent; + } - /// - /// Schedules a callback to run when the antecedent task completes. - /// - /// The callback to invoke. - public void OnCompleted(Action continuation) - { - Requires.NotNull(continuation, nameof(continuation)); - - this.antecedent.ContinueWith( - (_, s) => ((Action)s!)(), - continuation, - CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default); - } + /// + /// Gets a value indicating whether the antedent has already completed. + /// + public bool IsCompleted => this.antecedent.IsCompleted; + + /// + /// Rethrows any exception thrown by the antecedent. + /// + public T GetResult() => this.antecedent.GetAwaiter().GetResult(); + + /// + /// Schedules a callback to run when the antecedent task completes. + /// + /// The callback to invoke. + public void OnCompleted(Action continuation) + { + Requires.NotNull(continuation, nameof(continuation)); + + this.antecedent.ContinueWith( + (_, s) => ((Action)s!)(), + continuation, + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); } + } + /// + /// Provides a dedicated thread for requesting registry change notifications. + /// + /// + /// For versions of Windows prior to Windows 8, requesting registry change notifications + /// required that the thread that made the request remain alive or else the watcher would + /// simply signal the event and stop watching for changes. + /// This class provides a single, dedicated thread for requesting such notifications + /// so that they don't get canceled when a thread happens to exit. + /// The dedicated thread is released when no one is watching the registry any more. + /// + private static class DownlevelRegistryWatcherSupport + { /// - /// Provides a dedicated thread for requesting registry change notifications. + /// The size of the stack allocated for a thread that expects to stay within just a few methods in depth. /// /// - /// For versions of Windows prior to Windows 8, requesting registry change notifications - /// required that the thread that made the request remain alive or else the watcher would - /// simply signal the event and stop watching for changes. - /// This class provides a single, dedicated thread for requesting such notifications - /// so that they don't get canceled when a thread happens to exit. - /// The dedicated thread is released when no one is watching the registry any more. + /// The default stack size for a thread is 1MB. /// - private static class DownlevelRegistryWatcherSupport - { - /// - /// The size of the stack allocated for a thread that expects to stay within just a few methods in depth. - /// - /// - /// The default stack size for a thread is 1MB. - /// - private const int SmallThreadStackSize = 100 * 1024; + private const int SmallThreadStackSize = 100 * 1024; - /// - /// The object to lock when accessing any fields. - /// This is also the object that is waited on by the dedicated thread, - /// and may be pulsed by others to wake the dedicated thread to do some work. - /// - private static readonly object SyncObject = new object(); + /// + /// The object to lock when accessing any fields. + /// This is also the object that is waited on by the dedicated thread, + /// and may be pulsed by others to wake the dedicated thread to do some work. + /// + private static readonly object SyncObject = new object(); - /// - /// A queue of actions the dedicated thread should take. - /// - private static readonly Queue>> PendingWork = new Queue>>(); + /// + /// A queue of actions the dedicated thread should take. + /// + private static readonly Queue>> PendingWork = new(); - /// - /// The number of callers that still have an interest in the survival of the dedicated thread. - /// The dedicated thread will exit when this value reaches 0. - /// - private static int keepAliveCount; + /// + /// The number of callers that still have an interest in the survival of the dedicated thread. + /// The dedicated thread will exit when this value reaches 0. + /// + private static int keepAliveCount; - /// - /// The thread that should stay alive and be dequeuing . - /// - private static Thread? liveThread; + /// + /// The thread that should stay alive and be dequeuing . + /// + private static Thread? liveThread; - /// - /// Executes some action on a long-lived thread. - /// - /// The delegate to execute. - /// - /// A task that either faults with the exception thrown by - /// or completes after successfully executing the delegate - /// with a result that should be disposed when it is safe to terminate the long-lived thread. - /// - /// - /// This thread never posts to , so it is safe - /// to call this method and synchronously block on its result. - /// - internal static async Task ExecuteOnDedicatedThreadAsync(Action action) - { - Requires.NotNull(action, nameof(action)); + /// + /// Executes some action on a long-lived thread. + /// + /// The delegate to execute. + /// + /// A task that either faults with the exception thrown by + /// or completes after successfully executing the delegate + /// with a result that should be disposed when it is safe to terminate the long-lived thread. + /// + /// + /// This thread never posts to , so it is safe + /// to call this method and synchronously block on its result. + /// + internal static async Task ExecuteOnDedicatedThreadAsync(Action action) + { + Requires.NotNull(action, nameof(action)); - var tcs = new TaskCompletionSource(); - bool keepAliveCountIncremented = false; - try + var tcs = new TaskCompletionSource(); + bool keepAliveCountIncremented = false; + try + { + lock (SyncObject) { - lock (SyncObject) + PendingWork.Enqueue(Tuple.Create(action, tcs)); + + try + { + // This block intentionally left blank. + } + finally { - PendingWork.Enqueue(Tuple.Create(action, tcs)); + // We make these two assignments within a finally block + // to guard against an untimely ThreadAbortException causing + // us to execute just one of them. + keepAliveCountIncremented = true; + ++keepAliveCount; + } - try - { - // This block intentionally left blank. - } - finally - { - // We make these two assignments within a finally block - // to guard against an untimely ThreadAbortException causing - // us to execute just one of them. - keepAliveCountIncremented = true; - ++keepAliveCount; - } - - if (keepAliveCount == 1) - { - Assumes.Null(liveThread); - liveThread = new Thread(Worker, SmallThreadStackSize) - { - IsBackground = true, - Name = "Registry watcher", - }; - liveThread.Start(); - } - else + if (keepAliveCount == 1) + { + Assumes.Null(liveThread); + liveThread = new Thread(Worker, SmallThreadStackSize) { - // There *could* temporarily be multiple threads in some race conditions. - // Pulse all of them so that the live one is sure to get the message. - Monitor.PulseAll(SyncObject); - } + IsBackground = true, + Name = "Registry watcher", + }; + liveThread.Start(); } + else + { + // There *could* temporarily be multiple threads in some race conditions. + // Pulse all of them so that the live one is sure to get the message. + Monitor.PulseAll(SyncObject); + } + } - await tcs.Task.ConfigureAwait(false); - return new ThreadHandleRelease(); + await tcs.Task.ConfigureAwait(false); + return new ThreadHandleRelease(); + } + catch + { + if (keepAliveCountIncremented) + { + // Our caller will never have a chance to release their claim on the dedicated thread, + // so do it for them. + ReleaseRefOnDedicatedThread(); } - catch + + throw; + } + } + + /// + /// Decrements the count of interested parties in the live thread, + /// and helps it to terminate if necessary. + /// + private static void ReleaseRefOnDedicatedThread() + { + lock (SyncObject) + { + if (--keepAliveCount == 0) { - if (keepAliveCountIncremented) - { - // Our caller will never have a chance to release their claim on the dedicated thread, - // so do it for them. - ReleaseRefOnDedicatedThread(); - } + liveThread = null; - throw; + // Wake up any obsolete thread(s) so they can go to exit. + Monitor.PulseAll(SyncObject); } } + } - /// - /// Decrements the count of interested parties in the live thread, - /// and helps it to terminate if necessary. - /// - private static void ReleaseRefOnDedicatedThread() + /// + /// Executes thread-affinitized work from a queue until both the queue is empty + /// and any lingering interest in the survival of the dedicated thread has been released. + /// + /// + /// This method serves as the for our dedicated thread. + /// + private static void Worker() + { + while (true) { + Tuple>? work = null; lock (SyncObject) { - if (--keepAliveCount == 0) + if (Thread.CurrentThread != liveThread) { - liveThread = null; + // Regardless of our PendingWork and keepAliveCount, + // it isn't meant for this thread any more. + // This happens when keepAliveCount (at least temporarily) + // hits 0, so this thread must be assumed to be on its exit path, + // and another thread will be spawned to process new requests. + Assumes.True(liveThread is object || (keepAliveCount == 0 && PendingWork.Count == 0)); + return; + } - // Wake up any obsolete thread(s) so they can go to exit. - Monitor.PulseAll(SyncObject); + if (PendingWork.Count > 0) + { + work = PendingWork.Dequeue(); + } + else if (keepAliveCount == 0) + { + // No work, and no reason to stay alive. Exit the thread. + return; + } + else + { + // Sleep until another thread wants to wake us up with a Pulse. + Monitor.Wait(SyncObject); } } - } - /// - /// Executes thread-affinitized work from a queue until both the queue is empty - /// and any lingering interest in the survival of the dedicated thread has been released. - /// - /// - /// This method serves as the for our dedicated thread. - /// - private static void Worker() - { - while (true) + if (work is object) { - Tuple>? work = null; - lock (SyncObject) + try { - if (Thread.CurrentThread != liveThread) - { - // Regardless of our PendingWork and keepAliveCount, - // it isn't meant for this thread any more. - // This happens when keepAliveCount (at least temporarily) - // hits 0, so this thread must be assumed to be on its exit path, - // and another thread will be spawned to process new requests. - Assumes.True(liveThread is object || (keepAliveCount == 0 && PendingWork.Count == 0)); - return; - } - - if (PendingWork.Count > 0) - { - work = PendingWork.Dequeue(); - } - else if (keepAliveCount == 0) - { - // No work, and no reason to stay alive. Exit the thread. - return; - } - else - { - // Sleep until another thread wants to wake us up with a Pulse. - Monitor.Wait(SyncObject); - } + work.Item1(); + work.Item2.SetResult(EmptyStruct.Instance); } - - if (work is object) + catch (Exception ex) { - try - { - work.Item1(); - work.Item2.SetResult(EmptyStruct.Instance); - } - catch (Exception ex) - { - work.Item2.SetException(ex); - } + work.Item2.SetException(ex); } } } + } + + /// + /// Decrements the dedicated thread use counter by at most one upon disposal. + /// + private class ThreadHandleRelease : IDisposable + { + /// + /// A value indicating whether this instance has already been disposed. + /// + private bool disposed; /// - /// Decrements the dedicated thread use counter by at most one upon disposal. + /// Release the keep alive count reserved by this instance. /// - private class ThreadHandleRelease : IDisposable + public void Dispose() { - /// - /// A value indicating whether this instance has already been disposed. - /// - private bool disposed; - - /// - /// Release the keep alive count reserved by this instance. - /// - public void Dispose() + lock (SyncObject) { - lock (SyncObject) + if (!this.disposed) { - if (!this.disposed) - { - this.disposed = true; - ReleaseRefOnDedicatedThread(); - } + this.disposed = true; + ReleaseRefOnDedicatedThread(); } } } diff --git a/src/Microsoft.VisualStudio.Threading/Boxed.cs b/src/Microsoft.VisualStudio.Threading/Boxed.cs new file mode 100644 index 000000000..954140d6f --- /dev/null +++ b/src/Microsoft.VisualStudio.Threading/Boxed.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.Threading; + +internal static class Boxed +{ + /// + /// Returns an object containing . + /// + public static readonly object True = true; + + /// + /// Returns an object containing . + /// + public static readonly object False = false; + + /// + /// Returns an object containing specified value. + /// + public static object Box(bool value) + { + return value ? True : False; + } +} diff --git a/src/Microsoft.VisualStudio.Threading/CancellableJoinComputation.cs b/src/Microsoft.VisualStudio.Threading/CancellableJoinComputation.cs index 55f178da2..b121d2c1c 100644 --- a/src/Microsoft.VisualStudio.Threading/CancellableJoinComputation.cs +++ b/src/Microsoft.VisualStudio.Threading/CancellableJoinComputation.cs @@ -1,373 +1,374 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// Represents a possible shared computation task, which can be joined by multiple consumers with a cancellation token. +/// The overall computation is cancelled if all its consumers are cancelled. +/// +internal class CancellableJoinComputation { - using System; - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - using System.Threading; - using System.Threading.Tasks; + /// + /// The object to acquire a Monitor-style lock on for all field access on this instance. + /// + private readonly object syncObject = new object(); /// - /// Represents a possible shared computation task, which can be joined by multiple consumers with a cancellation token. - /// The overall computation is cancelled if all its consumers are cancelled. + /// A list of task completion sources which represents joined waiting requests with cancellable cancellation tokens. + /// When an individual cancellation token is triggered, we may allow that specific waiting task to continue, and only all of them abandon the computation then we may + /// cancel the inner computation. /// - internal class CancellableJoinComputation - { - /// - /// The object to acquire a Monitor-style lock on for all field access on this instance. - /// - private readonly object syncObject = new object(); + private List? joinedWaitingList; - /// - /// A list of task completion sources which represents joined waiting requests with cancellable cancellation tokens. - /// When an individule cancellation token is triggered, we may allow that specific waiting task to continue, and only all of them abandone the computation then we may - /// cancel the inner computation. - /// - private List? joinedWaitingList; + /// + /// A combined cancellation token source to cancel the inner computation task. + /// + private CancellationTokenSource? combinedCancellationTokenSource; - /// - /// A combined cancellation token source to cancel the inner computation task. - /// - private CancellationTokenSource? combinedCancellationTokenSource; + /// + /// The number of waiting requests which are not cancelled. It is only meaningful when is true. + /// + private int outstandingWaitingCount; - /// - /// The number of waiting requests which are not cancelled. It is only meaningful when is true. - /// - private int outstandingWaitingCount; + /// + /// Whether the inner task can be cancelled. If one waiting request is not cancellable, the inner task cannot be cancelled. + /// + private bool isCancellationAllowed; - /// - /// Whether the inner task can be cancelled. If one waiting request is not cancellable, the inner task cannot be cancelled. - /// - private bool isCancellationAllowed; + /// + /// Whether the cancellation of the inner task is requested. A new waiting request will not be allowed, once it is true. + /// + private bool isCancellationRequested; - /// - /// Whether the cancellation of the inner task is requested. A new waiting request will not be allowed, once it is true. - /// - private bool isCancellationRequested; + /// + /// Initializes a new instance of the class. + /// + /// A callback to create the task. + /// Whether the inner task can be cancelled. + internal CancellableJoinComputation(Func taskFactory, bool allowCancelled) + { + Requires.NotNull(taskFactory, nameof(taskFactory)); - /// - /// Initializes a new instance of the class. - /// - /// A callback to create the task. - /// Whether the inner task can be cancelled. - internal CancellableJoinComputation(Func taskFactory, bool allowCancelled) + if (allowCancelled) { - Requires.NotNull(taskFactory, nameof(taskFactory)); + this.isCancellationAllowed = true; + this.combinedCancellationTokenSource = new CancellationTokenSource(); + this.joinedWaitingList = new List(capacity: 2); + } - if (allowCancelled) - { - this.isCancellationAllowed = true; - this.combinedCancellationTokenSource = new CancellationTokenSource(); - this.joinedWaitingList = new List(capacity: 2); - } + this.InnerTask = taskFactory(this.combinedCancellationTokenSource?.Token ?? CancellationToken.None); - this.InnerTask = taskFactory(this.combinedCancellationTokenSource?.Token ?? CancellationToken.None); + if (allowCancelled) + { + // Note: this continuation is chained asynchronously to prevent being inlined when we trigger the combined cancellation token. + this.InnerTask.ContinueWith( + (t, s) => + { + var me = (CancellableJoinComputation)s!; - if (allowCancelled) - { - // Note: this continuation is chained asynchronously to prevent being inlined when we trigger the combined cancellation token. - this.InnerTask.ContinueWith( - (t, s) => + List allWaitingTasks; + CancellationTokenSource? combinedCancellationTokenSource; + lock (me.syncObject) { - var me = (CancellableJoinComputation)s!; + Assumes.NotNull(me.joinedWaitingList); - List allWaitingTasks; - CancellationTokenSource? combinedCancellationTokenSource; - lock (me.syncObject) - { - Assumes.NotNull(me.joinedWaitingList); - - allWaitingTasks = me.joinedWaitingList; - combinedCancellationTokenSource = me.combinedCancellationTokenSource; + allWaitingTasks = me.joinedWaitingList; + combinedCancellationTokenSource = me.combinedCancellationTokenSource; - me.joinedWaitingList = null; - me.combinedCancellationTokenSource = null; - } + me.joinedWaitingList = null; + me.combinedCancellationTokenSource = null; + } - combinedCancellationTokenSource?.Dispose(); + combinedCancellationTokenSource?.Dispose(); - if (t.IsCanceled) + if (t.IsCanceled) + { + for (int i = 0; i < allWaitingTasks.Count; i++) { - for (int i = 0; i < allWaitingTasks.Count; i++) + WaitingCancellationStatus status = allWaitingTasks[i]; + if (status.CancellationToken.IsCancellationRequested) { - WaitingCancellationStatus status = allWaitingTasks[i]; - if (status.CancellationToken.IsCancellationRequested) - { - status.TrySetCanceled(status.CancellationToken); - } - else - { - status.TrySetCanceled(); - } - - status.Dispose(); + status.TrySetCanceled(status.CancellationToken); } - } - else if (t.IsFaulted) - { - System.Collections.ObjectModel.ReadOnlyCollection exceptions = t.Exception!.InnerExceptions; - for (int i = 0; i < allWaitingTasks.Count; i++) + else { - WaitingCancellationStatus status = allWaitingTasks[i]; - status.TrySetException(exceptions); - status.Dispose(); + status.TrySetCanceled(); } + + status.Dispose(); } - else + } + else if (t.IsFaulted) + { + System.Collections.ObjectModel.ReadOnlyCollection exceptions = t.Exception!.InnerExceptions; + for (int i = 0; i < allWaitingTasks.Count; i++) { - for (int i = 0; i < allWaitingTasks.Count; i++) - { - WaitingCancellationStatus status = allWaitingTasks[i]; - status.TrySetResult(true); - status.Dispose(); - } + WaitingCancellationStatus status = allWaitingTasks[i]; + status.TrySetException(exceptions); + status.Dispose(); } - }, - this, - CancellationToken.None, - TaskContinuationOptions.RunContinuationsAsynchronously, - TaskScheduler.Default).Forget(); - } + } + else + { + for (int i = 0; i < allWaitingTasks.Count; i++) + { + WaitingCancellationStatus status = allWaitingTasks[i]; + status.TrySetResult(true); + status.Dispose(); + } + } + }, + this, + CancellationToken.None, + TaskContinuationOptions.RunContinuationsAsynchronously, + TaskScheduler.Default).Forget(); } + } - /// - /// Gets the inner computation task. - /// - internal Task InnerTask { get; } + /// + /// Gets the inner computation task. + /// + internal Task InnerTask { get; } - /// - /// Try to join the computation. - /// - /// It is true for the initial task starting the computation. This must be called once right after the constructor. - /// Returns a task which can be waited on. - /// A cancellation token to abort this waiting. - /// It returns false, if the inner task is aborted. In which case, no way to join the existing computation. - internal bool TryJoinComputation(bool isInitialTask, [NotNullWhen(true)] out Task? task, CancellationToken cancellationToken) + /// + /// Try to join the computation. + /// + /// It is true for the initial task starting the computation. This must be called once right after the constructor. + /// Returns a task which can be waited on. + /// A task scheduler for continuation. + /// A cancellation token to abort this waiting. + /// It returns false, if the inner task is aborted. In which case, no way to join the existing computation. + internal bool TryJoinComputation(bool isInitialTask, [NotNullWhen(true)] out Task? task, TaskScheduler taskScheduler, CancellationToken cancellationToken) + { + if (!this.isCancellationAllowed) { - if (!this.isCancellationAllowed) - { - task = this.JoinNotCancellableTaskAsync(isInitialTask, cancellationToken); - return true; - } + task = this.JoinNotCancellableTaskAsync(isInitialTask, taskScheduler, cancellationToken); + return true; + } - if (cancellationToken.IsCancellationRequested) + if (cancellationToken.IsCancellationRequested) + { + if (isInitialTask) { - if (isInitialTask) + // It is a corner case the cancellation token is triggered right after the first task starts. It may need cancel the inner task. + CancellationTokenSource? cancellationTokenSource = null; + lock (this.syncObject) { - // It is a corner case the cancellation token is triggered right after the first task starts. It may need cancel the inner task. - CancellationTokenSource? cancellationTokenSource = null; - lock (this.syncObject) - { - if (this.isCancellationAllowed && this.outstandingWaitingCount == 0 && this.combinedCancellationTokenSource is not null) - { - this.isCancellationRequested = true; - cancellationTokenSource = this.combinedCancellationTokenSource; - this.combinedCancellationTokenSource = null; - } - } - - if (cancellationTokenSource is not null) + if (this.isCancellationAllowed && this.outstandingWaitingCount == 0 && this.combinedCancellationTokenSource is not null) { - cancellationTokenSource.Cancel(); - cancellationTokenSource.Dispose(); + this.isCancellationRequested = true; + cancellationTokenSource = this.combinedCancellationTokenSource; + this.combinedCancellationTokenSource = null; } - - task = this.InnerTask; - return true; } - else + + if (cancellationTokenSource is not null) { - task = Task.FromCanceled(cancellationToken); - return true; + cancellationTokenSource.Cancel(); + cancellationTokenSource.Dispose(); } + + task = this.InnerTask; + return true; } + else + { + task = Task.FromCanceled(cancellationToken); + return true; + } + } - // if the inner task is joined by a new uncancellable task, we will abandone the cancellation token source because we will never use it anymore. - // we do it outside of our lock. - CancellationTokenSource? combinedCancellationTokenSourceToDispose = null; + // if the inner task is joined by a new not cancellable task, we will abandon the cancellation token source because we will never use it anymore. + // we do it outside of our lock. + CancellationTokenSource? combinedCancellationTokenSourceToDispose = null; - try + try + { + lock (this.syncObject) { - lock (this.syncObject) + if (this.isCancellationRequested) { - if (this.isCancellationRequested) - { - // If the earlier computation is aborted, we cannot join it anymore. - task = null; - return false; - } + // If the earlier computation is aborted, we cannot join it anymore. + task = null; + return false; + } - if (this.InnerTask.IsCompleted) - { - task = this.InnerTask; - return true; - } + if (this.InnerTask.IsCompleted) + { + task = this.InnerTask; + return true; + } - if (!cancellationToken.CanBeCanceled) - { - // A single joined client which doesn't allow cancellation would turn the entire computation not cancellable. - combinedCancellationTokenSourceToDispose = this.combinedCancellationTokenSource; - this.combinedCancellationTokenSource = null; + if (!cancellationToken.CanBeCanceled) + { + // A single joined client which doesn't allow cancellation would turn the entire computation not cancellable. + combinedCancellationTokenSourceToDispose = this.combinedCancellationTokenSource; + this.combinedCancellationTokenSource = null; - this.isCancellationAllowed = false; + this.isCancellationAllowed = false; - task = this.JoinNotCancellableTaskAsync(isInitialTask, CancellationToken.None); - } - else if (!this.isCancellationAllowed) + task = this.JoinNotCancellableTaskAsync(isInitialTask, taskScheduler, CancellationToken.None); + } + else if (!this.isCancellationAllowed) + { + task = this.JoinNotCancellableTaskAsync(isInitialTask, taskScheduler, cancellationToken); + } + else + { + Assumes.NotNull(this.joinedWaitingList); + + WaitingCancellationStatus status; + + // we need increase the outstanding count before creating WaitingCancellationStatus. + // Under a rare race condition the cancellation token can be trigger with this time frame, and lead OnWaitingTaskCancelled to be called recursively + // within this lock. It would be critical to make sure the outstandingWaitingCount to increase before decreasing there. + this.outstandingWaitingCount++; + try { - task = this.JoinNotCancellableTaskAsync(isInitialTask, cancellationToken); + status = new WaitingCancellationStatus(this, cancellationToken); } - else + catch { - Assumes.NotNull(this.joinedWaitingList); - - WaitingCancellationStatus status; - - // we need increase the outstanding count before creating WiatingCancellationStatus. - // Under a rare race condition the cancellation token can be trigger with this time frame, and lead OnWaitingTaskCancelled to be called recursively - // within this lock. It would be critical to make sure the outstandingWaitingCount to increase before decreasing there. - this.outstandingWaitingCount++; - try - { - status = new WaitingCancellationStatus(this, cancellationToken); - } - catch - { - this.outstandingWaitingCount--; - throw; - } + this.outstandingWaitingCount--; + throw; + } - this.joinedWaitingList.Add(status); + this.joinedWaitingList.Add(status); - task = status.Task; - } + task = status.Task; } } - finally - { - combinedCancellationTokenSourceToDispose?.Dispose(); - } + } + finally + { + combinedCancellationTokenSourceToDispose?.Dispose(); + } - return true; + return true; + } + + /// + /// A simple way to join if the inner task cannot be cancelled. + /// + /// Whether it is the first task to start the computation. + /// A task scheduler for continuation. + /// A cancellation token to abort the waiting. + /// A task to complete when the computation ends. + private Task JoinNotCancellableTaskAsync(bool isInitialTask, TaskScheduler taskScheduler, CancellationToken cancellationToken) + { + if (this.InnerTask.IsCompleted || (isInitialTask && !cancellationToken.CanBeCanceled)) + { + // Note: we don't reuse the inner task directly even for second request which is not cancellable. + // This is to prevent two task synchronized continuations, which can be blocking each other causing unexpected deadlocks. + // Adding an extra async task continuation prevents this problem, because all async continuation will be queued before calling synchronized task continuations, + // which are called one by one. + return this.InnerTask; } - /// - /// A simple way to join if the inner task cannot be cancelled. - /// - /// Whether it is the first task to start the computation. - /// A cancellation token to abort the waiting. - /// A task to complete when the computation ends. - private Task JoinNotCancellableTaskAsync(bool isInitialTask, CancellationToken cancellationToken) + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + // We tack cancellation onto the task that we actually return to the caller. + // This doesn't cancel resource preparation, but it does allow the caller to return early + // in the event of their own cancellation token being canceled. + return this.InnerTask.ContinueWith( + t => t.GetAwaiter().GetResult(), + cancellationToken, + TaskContinuationOptions.RunContinuationsAsynchronously, + taskScheduler); + } + + /// + /// Handles one waiting task can be cancelled. + /// + /// The status of the waiting task being cancelled. + private void OnWaitingTaskCancelled(WaitingCancellationStatus status) + { + CancellationTokenSource? overallCancellationSource = null; + + lock (this.syncObject) { - if (this.InnerTask.IsCompleted || (isInitialTask && !cancellationToken.CanBeCanceled)) + if (--this.outstandingWaitingCount != 0 || !this.isCancellationAllowed) { - // Note: we don't reuse the inner task directly even for second request which is not cancellable. - // This is to prevent two task sychorized continuations, which can be blocking each other causing unexpected deadlocks. - // Adding an extra async task continuation prevents this problem, because all async continuation will be queued before calling synchronized task continuations, - // which are called one by one. - return this.InnerTask; + // when overall cancellation is not allowed, we cancel this single waiting task. + status.TrySetCanceled(status.CancellationToken); } - - if (cancellationToken.IsCancellationRequested) + else { - return Task.FromCanceled(cancellationToken); + // otherwise, we cancel the overall computation, when it is done, it will cancel the current waiting task. + overallCancellationSource = this.combinedCancellationTokenSource; + if (overallCancellationSource is not null) + { + this.combinedCancellationTokenSource = null; + this.isCancellationRequested = true; + } } + } - // We tack cancellation onto the task that we actually return to the caller. - // This doesn't cancel resource preparation, but it does allow the caller to return early - // in the event of their own cancellation token being canceled. - return this.InnerTask.ContinueWith( - t => t.GetAwaiter().GetResult(), - cancellationToken, - TaskContinuationOptions.RunContinuationsAsynchronously, - TaskScheduler.Default); + if (overallCancellationSource is not null) + { + overallCancellationSource.Cancel(); + overallCancellationSource.Dispose(); } + } + /// + /// Represents the status of a single request waiting the inner task to complete. + /// + private class WaitingCancellationStatus : TaskCompletionSource + { /// - /// Handles one waiting task can be cancelled. + /// The cancellation registration to handle the cancellation token of the request. /// - /// The status of the waiting task being cancelled. - private void OnWaitingTaskCancelled(WaitingCancellationStatus status) + private readonly CancellationTokenRegistration cancellationTokenRegistration; + + /// + /// Initializes a new instance of the class. + /// + /// The joined computation. + /// The cancellation token of the request. + internal WaitingCancellationStatus(CancellableJoinComputation computation, CancellationToken cancellationToken) + : base(computation, TaskCreationOptions.RunContinuationsAsynchronously) { - CancellationTokenSource? overallCancellationSource = null; + Assumes.True(cancellationToken.CanBeCanceled); + this.CancellationToken = cancellationToken; - lock (this.syncObject) - { - if (--this.outstandingWaitingCount != 0 || !this.isCancellationAllowed) + this.cancellationTokenRegistration = cancellationToken.Register( + s => { - // when overall cancellation is not allowed, we cancel this single waiting task. - status.TrySetCanceled(status.CancellationToken); - } - else - { - // otherwise, we cancel the overall computation, when it is done, it will cancel the current waiting task. - overallCancellationSource = this.combinedCancellationTokenSource; - if (overallCancellationSource is not null) - { - this.combinedCancellationTokenSource = null; - this.isCancellationRequested = true; - } - } - } - - if (overallCancellationSource is not null) - { - overallCancellationSource.Cancel(); - overallCancellationSource.Dispose(); - } + var me = (WaitingCancellationStatus)s!; + me.Computation.OnWaitingTaskCancelled(me); + }, + this, + useSynchronizationContext: false); } /// - /// Represents the status of a single request waiting the inner task to complete. + /// Gets the joined computation. + /// Note: we set it to the state of the TaskCompletionSource. It makes it easy to trace it through the waiting task in dump files. /// - private class WaitingCancellationStatus : TaskCompletionSource - { - /// - /// The cancellation registration to handle the cancellation token of the request. - /// - private CancellationTokenRegistration cancellationTokenRegistration; - - /// - /// Initializes a new instance of the class. - /// - /// The joined computation. - /// The cancellation token of the request. - internal WaitingCancellationStatus(CancellableJoinComputation computation, CancellationToken cancellationToken) - : base(computation, TaskCreationOptions.RunContinuationsAsynchronously) - { - Assumes.True(cancellationToken.CanBeCanceled); - this.CancellationToken = cancellationToken; + internal CancellableJoinComputation Computation => (CancellableJoinComputation)this.Task.AsyncState!; - this.cancellationTokenRegistration = cancellationToken.Register( - s => - { - var me = (WaitingCancellationStatus)s!; - me.Computation.OnWaitingTaskCancelled(me); - }, - this, - useSynchronizationContext: false); - } + /// + /// Gets the cancellation token of the waiting task. + /// + internal CancellationToken CancellationToken { get; } - /// - /// Gets the joined computation. - /// Note: we set it to the state of the TaskCompletionSource. It makes it easy to trace it through the waiting task in dump files. - /// - internal CancellableJoinComputation Computation => (CancellableJoinComputation)this.Task.AsyncState!; - - /// - /// Gets the cancellation token of the waiting task. - /// - internal CancellationToken CancellationToken { get; } - - /// - /// Dispose this instance. - /// - public void Dispose() - { - this.cancellationTokenRegistration.Dispose(); - } + /// + /// Dispose this instance. + /// + public void Dispose() + { + this.cancellationTokenRegistration.Dispose(); } } } diff --git a/src/Microsoft.VisualStudio.Threading/CancellationTokenExtensions.cs b/src/Microsoft.VisualStudio.Threading/CancellationTokenExtensions.cs index 4b487f435..af6649e29 100644 --- a/src/Microsoft.VisualStudio.Threading/CancellationTokenExtensions.cs +++ b/src/Microsoft.VisualStudio.Threading/CancellationTokenExtensions.cs @@ -1,202 +1,207 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading -{ - using System; - using System.Threading; +using System; +using System.Threading; + +namespace Microsoft.VisualStudio.Threading; +/// +/// Extensions to . +/// +public static class CancellationTokenExtensions +{ /// - /// Extensions to . + /// Creates a new that is canceled when any of a set of other tokens are canceled. /// - public static class CancellationTokenExtensions + /// The first token. + /// The second token. + /// A struct that contains the combined and a means to release memory when you're done using it. + public static CombinedCancellationToken CombineWith(this CancellationToken original, CancellationToken other) { - /// - /// Creates a new that is canceled when any of a set of other tokens are canceled. - /// - /// The first token. - /// The second token. - /// A struct that contains the combined and a means to release memory when you're done using it. - public static CombinedCancellationToken CombineWith(this CancellationToken original, CancellationToken other) + if (original.IsCancellationRequested || !other.CanBeCanceled) { - if (original.IsCancellationRequested || !other.CanBeCanceled) - { - return new CombinedCancellationToken(original); - } - - if (other.IsCancellationRequested || !original.CanBeCanceled) - { - return new CombinedCancellationToken(other); - } + return new CombinedCancellationToken(original); + } - // This is the most expensive path to take since it involves allocating memory and requiring disposal. - // Before this point we've checked every condition that would allow us to avoid it. - return new CombinedCancellationToken(CancellationTokenSource.CreateLinkedTokenSource(original, other)); + if (other.IsCancellationRequested || !original.CanBeCanceled) + { + return new CombinedCancellationToken(other); } - /// - /// Creates a new that is canceled when any of a set of other tokens are canceled. - /// - /// The first token. - /// The additional tokens. - /// A struct that contains the combined and a means to release memory when you're done using it. - public static CombinedCancellationToken CombineWith(this CancellationToken original, params CancellationToken[] others) + // This is the most expensive path to take since it involves allocating memory and requiring disposal. + // Before this point we've checked every condition that would allow us to avoid it. + return new CombinedCancellationToken(CancellationTokenSource.CreateLinkedTokenSource(original, other)); + } + + /// + /// Creates a new that is canceled when any of a set of other tokens are canceled. + /// + /// The first token. + /// The additional tokens. + /// A struct that contains the combined and a means to release memory when you're done using it. + public static CombinedCancellationToken CombineWith(this CancellationToken original, params CancellationToken[] others) + { + Requires.NotNull(others, nameof(others)); + + if (original.IsCancellationRequested) { - Requires.NotNull(others, nameof(others)); + return new CombinedCancellationToken(original); + } - if (original.IsCancellationRequested) + int cancelableTokensCount = original.CanBeCanceled ? 1 : 0; + foreach (CancellationToken other in others) + { + if (other.IsCancellationRequested) { - return new CombinedCancellationToken(original); + return new CombinedCancellationToken(other); } - int cancelableTokensCount = original.CanBeCanceled ? 1 : 0; - foreach (CancellationToken other in others) + if (other.CanBeCanceled) { - if (other.IsCancellationRequested) - { - return new CombinedCancellationToken(other); - } + cancelableTokensCount++; + } + } - if (other.CanBeCanceled) + switch (cancelableTokensCount) + { + case 0: + return new CombinedCancellationToken(CancellationToken.None); + case 1: + if (original.CanBeCanceled) { - cancelableTokensCount++; + return new CombinedCancellationToken(original); } - } - switch (cancelableTokensCount) - { - case 0: - return new CombinedCancellationToken(CancellationToken.None); - case 1: - if (original.CanBeCanceled) - { - return new CombinedCancellationToken(original); - } - - foreach (CancellationToken other in others) + foreach (CancellationToken other in others) + { + if (other.CanBeCanceled) { - if (other.CanBeCanceled) - { - return new CombinedCancellationToken(other); - } + return new CombinedCancellationToken(other); } + } - throw Assumes.NotReachable(); - case 2: - CancellationToken first = CancellationToken.None; - CancellationToken second = CancellationToken.None; + throw Assumes.NotReachable(); + case 2: + CancellationToken first = CancellationToken.None; + CancellationToken second = CancellationToken.None; - if (original.CanBeCanceled) - { - first = original; - } + if (original.CanBeCanceled) + { + first = original; + } - foreach (CancellationToken other in others) + foreach (CancellationToken other in others) + { + if (other.CanBeCanceled) { - if (other.CanBeCanceled) + if (first.CanBeCanceled) + { + second = other; + } + else { - if (first.CanBeCanceled) - { - second = other; - } - else - { - first = other; - } + first = other; } } + } - Assumes.True(first.CanBeCanceled && second.CanBeCanceled); + Assumes.True(first.CanBeCanceled && second.CanBeCanceled); - // Call the overload that takes two CancellationTokens explicitly to avoid an array allocation. - return new CombinedCancellationToken(CancellationTokenSource.CreateLinkedTokenSource(first, second)); - default: - // This is the most expensive path to take since it involves allocating memory and requiring disposal. - // Before this point we've checked every condition that would allow us to avoid it. - var cancelableTokens = new CancellationToken[cancelableTokensCount]; - int i = 0; - foreach (CancellationToken other in others) + // Call the overload that takes two CancellationTokens explicitly to avoid an array allocation. + return new CombinedCancellationToken(CancellationTokenSource.CreateLinkedTokenSource(first, second)); + default: + // This is the most expensive path to take since it involves allocating memory and requiring disposal. + // Before this point we've checked every condition that would allow us to avoid it. + var cancelableTokens = new CancellationToken[cancelableTokensCount]; + int i = 0; + if (original.CanBeCanceled) + { + cancelableTokens[i++] = original; + } + + foreach (CancellationToken other in others) + { + if (other.CanBeCanceled) { - if (other.CanBeCanceled) - { - cancelableTokens[i++] = other; - } + cancelableTokens[i++] = other; } + } - return new CombinedCancellationToken(CancellationTokenSource.CreateLinkedTokenSource(cancelableTokens)); - } + return new CombinedCancellationToken(CancellationTokenSource.CreateLinkedTokenSource(cancelableTokens)); } + } + + /// + /// Provides access to a that combines multiple other tokens, + /// and allows convenient disposal of any applicable . + /// + public readonly struct CombinedCancellationToken : IDisposable, IEquatable + { + /// + /// The object to dispose when this struct is disposed. + /// + private readonly CancellationTokenSource? cts; /// - /// Provides access to a that combines multiple other tokens, - /// and allows convenient disposal of any applicable . + /// Initializes a new instance of the struct + /// that contains an aggregate whose source must be disposed. /// - public readonly struct CombinedCancellationToken : IDisposable, IEquatable + /// The cancellation token source. + public CombinedCancellationToken(CancellationTokenSource cancellationTokenSource) { - /// - /// The object to dispose when this struct is disposed. - /// - private readonly CancellationTokenSource? cts; - - /// - /// Initializes a new instance of the struct - /// that contains an aggregate whose source must be disposed. - /// - /// The cancellation token source. - public CombinedCancellationToken(CancellationTokenSource cancellationTokenSource) - { - this.cts = cancellationTokenSource; - this.Token = cancellationTokenSource.Token; - } + Requires.NotNull(cancellationTokenSource); + this.cts = cancellationTokenSource; + this.Token = cancellationTokenSource.Token; + } - /// - /// Initializes a new instance of the struct - /// that represents just a single, non-disposable . - /// - /// The cancellation token. - public CombinedCancellationToken(CancellationToken cancellationToken) - { - this.cts = null; - this.Token = cancellationToken; - } + /// + /// Initializes a new instance of the struct + /// that represents just a single, non-disposable . + /// + /// The cancellation token. + public CombinedCancellationToken(CancellationToken cancellationToken) + { + this.cts = null; + this.Token = cancellationToken; + } - /// - /// Gets the combined cancellation token. - /// - public CancellationToken Token { get; } - - /// - /// Checks whether two instances of are equal. - /// - /// The left operand. - /// The right operand. - /// true if they are equal; false otherwise. - public static bool operator ==(CombinedCancellationToken left, CombinedCancellationToken right) => left.Equals(right); - - /// - /// Checks whether two instances of are not equal. - /// - /// The left operand. - /// The right operand. - /// true if they are not equal; false if they are equal. - public static bool operator !=(CombinedCancellationToken left, CombinedCancellationToken right) => !(left == right); - - /// - /// Disposes the behind this combined token, if any. - /// - public void Dispose() - { - this.cts?.Dispose(); - } + /// + /// Gets the combined cancellation token. + /// + public CancellationToken Token { get; } - /// - public override bool Equals(object? obj) => obj is CombinedCancellationToken other && this.Equals(other); + /// + /// Checks whether two instances of are equal. + /// + /// The left operand. + /// The right operand. + /// if they are equal; otherwise. + public static bool operator ==(CombinedCancellationToken left, CombinedCancellationToken right) => left.Equals(right); - /// - public bool Equals(CombinedCancellationToken other) => this.cts == other.cts && this.Token.Equals(other.Token); + /// + /// Checks whether two instances of are not equal. + /// + /// The left operand. + /// The right operand. + /// if they are not equal; if they are equal. + public static bool operator !=(CombinedCancellationToken left, CombinedCancellationToken right) => !(left == right); - /// - public override int GetHashCode() => (this.cts?.GetHashCode() ?? 0) + this.Token.GetHashCode(); + /// + /// Disposes the behind this combined token, if any. + /// + public void Dispose() + { + this.cts?.Dispose(); } + + /// + public override bool Equals(object? obj) => obj is CombinedCancellationToken other && this.Equals(other); + + /// + public bool Equals(CombinedCancellationToken other) => this.cts == other.cts && this.Token.Equals(other.Token); + + /// + public override int GetHashCode() => (this.cts?.GetHashCode() ?? 0) + this.Token.GetHashCode(); } } diff --git a/src/Microsoft.VisualStudio.Threading/DelegatingJoinableTaskFactory.cs b/src/Microsoft.VisualStudio.Threading/DelegatingJoinableTaskFactory.cs index d19af1791..a17438de7 100644 --- a/src/Microsoft.VisualStudio.Threading/DelegatingJoinableTaskFactory.cs +++ b/src/Microsoft.VisualStudio.Threading/DelegatingJoinableTaskFactory.cs @@ -1,83 +1,82 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// A JoinableTaskFactory base class for derived types that delegate some of their work to an existing instance. +/// +/// +/// All virtual methods default to calling into the inner for its behavior, +/// rather than the default behavior of the base class. +/// This is useful because a derived-type cannot call protected methods on another instance of that type. +/// +public class DelegatingJoinableTaskFactory : JoinableTaskFactory { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Text; - using System.Threading; - using System.Threading.Tasks; + /// + /// The inner factory that will create the tasks. + /// + private readonly JoinableTaskFactory innerFactory; /// - /// A JoinableTaskFactory base class for derived types that delegate some of their work to an existing instance. + /// Initializes a new instance of the class. /// - /// - /// All virtual methods default to calling into the inner for its behavior, - /// rather than the default behavior of the base class. - /// This is useful because a derived-type cannot call protected methods on another instance of that type. - /// - public class DelegatingJoinableTaskFactory : JoinableTaskFactory + /// The inner factory that will create the tasks. + protected DelegatingJoinableTaskFactory(JoinableTaskFactory innerFactory) + : base(Requires.NotNull(innerFactory, "innerFactory").Context, innerFactory.Collection) { - /// - /// The inner factory that will create the tasks. - /// - private readonly JoinableTaskFactory innerFactory; - - /// - /// Initializes a new instance of the class. - /// - /// The inner factory that will create the tasks. - protected DelegatingJoinableTaskFactory(JoinableTaskFactory innerFactory) - : base(Requires.NotNull(innerFactory, "innerFactory").Context, innerFactory.Collection) - { - this.innerFactory = innerFactory; - } + this.innerFactory = innerFactory; + } - /// - /// Synchronously blocks the calling thread for the completion of the specified task. - /// - /// The task whose completion is being waited on. - protected internal override void WaitSynchronously(Task task) - { - this.innerFactory.WaitSynchronously(task); - } + /// + /// Synchronously blocks the calling thread for the completion of the specified task. + /// + /// The task whose completion is being waited on. + protected internal override void WaitSynchronously(Task task) + { + this.innerFactory.WaitSynchronously(task); + } - /// - /// Posts a message to the specified underlying SynchronizationContext for processing when the main thread - /// is freely available. - /// - /// The callback to invoke. - /// State to pass to the callback. - protected internal override void PostToUnderlyingSynchronizationContext(SendOrPostCallback callback, object state) - { - this.innerFactory.PostToUnderlyingSynchronizationContext(callback, state); - } + /// + /// Posts a message to the specified underlying SynchronizationContext for processing when the main thread + /// is freely available. + /// + /// The callback to invoke. + /// State to pass to the callback. + protected internal override void PostToUnderlyingSynchronizationContext(SendOrPostCallback callback, object state) + { + this.innerFactory.PostToUnderlyingSynchronizationContext(callback, state); + } - /// - /// Raised when a joinable task has requested a transition to the main thread. - /// - /// The task requesting the transition to the main thread. - /// - /// This event may be raised on any thread, including the main thread. - /// - protected internal override void OnTransitioningToMainThread(JoinableTask joinableTask) - { - this.innerFactory.OnTransitioningToMainThread(joinableTask); - } + /// + /// Raised when a joinable task has requested a transition to the main thread. + /// + /// The task requesting the transition to the main thread. + /// + /// This event may be raised on any thread, including the main thread. + /// + protected internal override void OnTransitioningToMainThread(JoinableTask joinableTask) + { + this.innerFactory.OnTransitioningToMainThread(joinableTask); + } - /// - /// Raised whenever a joinable task has completed a transition to the main thread. - /// - /// The task whose request to transition to the main thread has completed. - /// A value indicating whether the transition was cancelled before it was fulfilled. - /// - /// This event is usually raised on the main thread, but can be on another thread when is true. - /// - protected internal override void OnTransitionedToMainThread(JoinableTask joinableTask, bool canceled) - { - this.innerFactory.OnTransitionedToMainThread(joinableTask, canceled); - } + /// + /// Raised whenever a joinable task has completed a transition to the main thread. + /// + /// The task whose request to transition to the main thread has completed. + /// A value indicating whether the transition was cancelled before it was fulfilled. + /// + /// This event is usually raised on the main thread, but can be on another thread when is . + /// + protected internal override void OnTransitionedToMainThread(JoinableTask joinableTask, bool canceled) + { + this.innerFactory.OnTransitionedToMainThread(joinableTask, canceled); } } diff --git a/src/Microsoft.VisualStudio.Threading/Dgml.cs b/src/Microsoft.VisualStudio.Threading/Dgml.cs index e47cb3a89..bb628a18f 100644 --- a/src/Microsoft.VisualStudio.Threading/Dgml.cs +++ b/src/Microsoft.VisualStudio.Threading/Dgml.cs @@ -1,294 +1,293 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Text; - using System.Threading.Tasks; - using System.Xml.Linq; - - internal static class Dgml - { - /// - /// The namespace that all DGML nodes appear in. - /// - internal const string Namespace = "http://schemas.microsoft.com/vs/2009/dgml"; - - private static readonly XName NodeName = XName.Get("Node", Namespace); - private static readonly XName NodesName = XName.Get("Nodes", Namespace); - private static readonly XName LinkName = XName.Get("Link", Namespace); - private static readonly XName LinksName = XName.Get("Links", Namespace); - private static readonly XName StylesName = XName.Get("Styles", Namespace); - private static readonly XName StyleName = XName.Get("Style", Namespace); - - internal static XDocument Create(out XElement nodes, out XElement links, string layout = "Sugiyama", string? direction = null) - { - var dgml = new XDocument(); - dgml.Add( - new XElement( - XName.Get("DirectedGraph", Namespace), - new XAttribute("Layout", layout))); - if (direction is object) - { - dgml.Root.Add(new XAttribute("GraphDirection", direction)); - } +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; - nodes = new XElement(XName.Get("Nodes", Namespace)); - links = new XElement(XName.Get("Links", Namespace)); - dgml.Root.Add(nodes); - dgml.Root.Add(links); - dgml.WithCategories(Category("Contains", isContainment: true)); - return dgml; - } +namespace Microsoft.VisualStudio.Threading; - internal static XDocument WithCategories(this XDocument document, params string[] categories) +internal static class Dgml +{ + /// + /// The namespace that all DGML nodes appear in. + /// + internal const string Namespace = "http://schemas.microsoft.com/vs/2009/dgml"; + + private static readonly XName NodeName = XName.Get("Node", Namespace); + private static readonly XName NodesName = XName.Get("Nodes", Namespace); + private static readonly XName LinkName = XName.Get("Link", Namespace); + private static readonly XName LinksName = XName.Get("Links", Namespace); + private static readonly XName StylesName = XName.Get("Styles", Namespace); + private static readonly XName StyleName = XName.Get("Style", Namespace); + + internal static XDocument Create(out XElement nodes, out XElement links, string layout = "Sugiyama", string? direction = null) + { + var dgml = new XDocument(); + dgml.Add( + new XElement( + XName.Get("DirectedGraph", Namespace), + new XAttribute("Layout", layout))); + if (direction is object) { - Requires.NotNull(document, nameof(document)); - Requires.NotNull(categories, nameof(categories)); - - GetRootElement(document, "Categories").Add(categories.Select(c => Category(c))); - return document; + dgml.Root!.Add(new XAttribute("GraphDirection", direction)); } - internal static XDocument WithCategories(this XDocument document, params XElement[] categories) - { - Requires.NotNull(document, nameof(document)); - Requires.NotNull(categories, nameof(categories)); - - GetRootElement(document, "Categories").Add(categories); - return document; - } + nodes = new XElement(XName.Get("Nodes", Namespace)); + links = new XElement(XName.Get("Links", Namespace)); + dgml.Root!.Add(nodes); + dgml.Root.Add(links); + dgml.WithCategories(Category("Contains", isContainment: true)); + return dgml; + } - internal static XElement Node(string? id = null, string? label = null, string? group = null) - { - var element = new XElement(NodeName); + internal static XDocument WithCategories(this XDocument document, params string[] categories) + { + Requires.NotNull(document, nameof(document)); + Requires.NotNull(categories, nameof(categories)); - if (!string.IsNullOrEmpty(id)) - { - element.SetAttributeValue("Id", id); - } + GetRootElement(document, "Categories").Add(categories.Select(c => Category(c))); + return document; + } - if (!string.IsNullOrEmpty(label)) - { - element.SetAttributeValue("Label", label); - } + internal static XDocument WithCategories(this XDocument document, params XElement[] categories) + { + Requires.NotNull(document, nameof(document)); + Requires.NotNull(categories, nameof(categories)); - if (!string.IsNullOrEmpty(group)) - { - element.SetAttributeValue("Group", group); - } + GetRootElement(document, "Categories").Add(categories); + return document; + } - return element; - } + internal static XElement Node(string? id = null, string? label = null, string? group = null) + { + var element = new XElement(NodeName); - internal static XDocument WithNode(this XDocument document, XElement node) + if (!string.IsNullOrEmpty(id)) { - Requires.NotNull(document, nameof(document)); - Requires.NotNull(node, nameof(node)); - - XElement? nodes = document.GetRootElement(NodesName); - nodes.Add(node); - return document; + element.SetAttributeValue("Id", id); } - internal static XElement Link(string source, string target) + if (!string.IsNullOrEmpty(label)) { - Requires.NotNullOrEmpty(source, nameof(source)); - Requires.NotNullOrEmpty(target, nameof(target)); - - return new XElement( - LinkName, - new XAttribute("Source", source), - new XAttribute("Target", target)); + element.SetAttributeValue("Label", label); } - internal static XElement Link(XElement source, XElement target) + if (!string.IsNullOrEmpty(group)) { - return Link(source.Attribute("Id").Value, target.Attribute("Id").Value); + element.SetAttributeValue("Group", group); } - internal static XDocument WithLink(this XDocument document, XElement link) - { - Requires.NotNull(document, nameof(document)); - Requires.NotNull(link, nameof(link)); + return element; + } - XElement? links = document.GetRootElement(LinksName); - links.Add(link); - return document; - } + internal static XDocument WithNode(this XDocument document, XElement node) + { + Requires.NotNull(document, nameof(document)); + Requires.NotNull(node, nameof(node)); - internal static XElement Category(string id, string? label = null, string? background = null, string? foreground = null, string? icon = null, bool isTag = false, bool isContainment = false) - { - Requires.NotNullOrEmpty(id, nameof(id)); + XElement? nodes = document.GetRootElement(NodesName); + nodes.Add(node); + return document; + } - var category = new XElement(XName.Get("Category", Namespace), new XAttribute("Id", id)); - if (!string.IsNullOrEmpty(label)) - { - category.SetAttributeValue("Label", label); - } + internal static XElement Link(string source, string target) + { + Requires.NotNullOrEmpty(source, nameof(source)); + Requires.NotNullOrEmpty(target, nameof(target)); - if (!string.IsNullOrEmpty(background)) - { - category.SetAttributeValue("Background", background); - } + return new XElement( + LinkName, + new XAttribute("Source", source), + new XAttribute("Target", target)); + } - if (!string.IsNullOrEmpty(foreground)) - { - category.SetAttributeValue("Foreground", foreground); - } + internal static XElement Link(XElement source, XElement target) + { + return Link(source.Attribute("Id")!.Value, target.Attribute("Id")!.Value); + } - if (!string.IsNullOrEmpty(icon)) - { - category.SetAttributeValue("Icon", icon); - } + internal static XDocument WithLink(this XDocument document, XElement link) + { + Requires.NotNull(document, nameof(document)); + Requires.NotNull(link, nameof(link)); - if (isTag) - { - category.SetAttributeValue("IsTag", "True"); - } + XElement? links = document.GetRootElement(LinksName); + links.Add(link); + return document; + } - if (isContainment) - { - category.SetAttributeValue("IsContainment", "True"); - } + internal static XElement Category(string id, string? label = null, string? background = null, string? foreground = null, string? icon = null, bool isTag = false, bool isContainment = false) + { + Requires.NotNullOrEmpty(id, nameof(id)); - return category; + var category = new XElement(XName.Get("Category", Namespace), new XAttribute("Id", id)); + if (!string.IsNullOrEmpty(label)) + { + category.SetAttributeValue("Label", label); } - internal static XElement Comment(string label) + if (!string.IsNullOrEmpty(background)) { - return Node(label: label).WithCategories("Comment"); + category.SetAttributeValue("Background", background); } - internal static XElement Container(string id, string? label = null) + if (!string.IsNullOrEmpty(foreground)) { - return Node(id, label, group: "Expanded"); + category.SetAttributeValue("Foreground", foreground); } - internal static XDocument WithContainers(this XDocument document, IEnumerable containers) + if (!string.IsNullOrEmpty(icon)) { - foreach (XElement? container in containers) - { - WithNode(document, container); - } - - return document; + category.SetAttributeValue("Icon", icon); } - internal static XElement ContainedBy(this XElement node, XElement container) + if (isTag) { - Requires.NotNull(node, nameof(node)); - Requires.NotNull(container, nameof(container)); - - Link(container, node).WithCategories("Contains"); - return node; + category.SetAttributeValue("IsTag", "True"); } - internal static XElement ContainedBy(this XElement node, string containerId, XDocument document) + if (isContainment) { - Requires.NotNull(node, nameof(node)); - Requires.NotNullOrEmpty(containerId, nameof(containerId)); - - document.WithLink(Link(containerId, node.Attribute("Id").Value).WithCategories("Contains")); - return node; + category.SetAttributeValue("IsContainment", "True"); } - /// - /// Adds categories to a DGML node or link. - /// - /// The node or link to add categories to. - /// The categories to add. - /// The same node that was passed in. To enable "fluent" syntax. - internal static XElement WithCategories(this XElement element, params string[] categories) - { - Requires.NotNull(element, nameof(element)); + return category; + } - foreach (var category in categories) - { - if (element.Attribute("Category") is null) - { - element.SetAttributeValue("Category", category); - } - else - { - element.Add(new XElement( - XName.Get("Category", Namespace), - new XAttribute("Ref", category))); - } - } + internal static XElement Comment(string label) + { + return Node(label: label).WithCategories("Comment"); + } - return element; - } + internal static XElement Container(string id, string? label = null) + { + return Node(id, label, group: "Expanded"); + } - internal static XDocument WithStyle(this XDocument document, string categoryId, IEnumerable> properties, string targetType = "Node") + internal static XDocument WithContainers(this XDocument document, IEnumerable containers) + { + foreach (XElement? container in containers) { - Requires.NotNull(document, nameof(document)); - Requires.NotNullOrEmpty(categoryId, nameof(categoryId)); - Requires.NotNull(properties, nameof(properties)); - Requires.NotNullOrEmpty(targetType, nameof(targetType)); + WithNode(document, container); + } - XElement? container = document.Root.Element(StylesName); - if (container is null) - { - document.Root.Add(container = new XElement(StylesName)); - } + return document; + } - var style = new XElement( - StyleName, - new XAttribute("TargetType", targetType), - new XAttribute("GroupLabel", categoryId), - new XElement(XName.Get("Condition", Namespace), new XAttribute("Expression", "HasCategory('" + categoryId + "')"))); - style.Add(properties.Select(p => new XElement(XName.Get("Setter", Namespace), new XAttribute("Property", p.Key), new XAttribute("Value", p.Value)))); + internal static XElement ContainedBy(this XElement node, XElement container) + { + Requires.NotNull(node, nameof(node)); + Requires.NotNull(container, nameof(container)); - container.Add(style); + Link(container, node).WithCategories("Contains"); + return node; + } - return document; - } + internal static XElement ContainedBy(this XElement node, string containerId, XDocument document) + { + Requires.NotNull(node, nameof(node)); + Requires.NotNullOrEmpty(containerId, nameof(containerId)); + + document.WithLink(Link(containerId, node.Attribute("Id")!.Value).WithCategories("Contains")); + return node; + } - internal static XDocument WithStyle(this XDocument document, string categoryId, string targetType = "Node", string? foreground = null, string? background = null, string? icon = null) + /// + /// Adds categories to a DGML node or link. + /// + /// The node or link to add categories to. + /// The categories to add. + /// The same node that was passed in. To enable "fluent" syntax. + internal static XElement WithCategories(this XElement element, params string[] categories) + { + Requires.NotNull(element, nameof(element)); + + foreach (var category in categories) { - var properties = new Dictionary(); - if (!string.IsNullOrEmpty(foreground)) + if (element.Attribute("Category") is null) { - properties.Add("Foreground", foreground); + element.SetAttributeValue("Category", category); } - - if (!string.IsNullOrEmpty(background)) + else { - properties.Add("Background", background); + element.Add(new XElement( + XName.Get("Category", Namespace), + new XAttribute("Ref", category))); } + } - if (!string.IsNullOrEmpty(icon)) - { - properties.Add("Icon", icon); - } + return element; + } - return WithStyle(document, categoryId, properties, targetType); - } + internal static XDocument WithStyle(this XDocument document, string categoryId, IEnumerable> properties, string targetType = "Node") + { + Requires.NotNull(document, nameof(document)); + Requires.NotNullOrEmpty(categoryId, nameof(categoryId)); + Requires.NotNull(properties, nameof(properties)); + Requires.NotNullOrEmpty(targetType, nameof(targetType)); - private static XElement GetRootElement(this XDocument document, XName name) + XElement? container = document.Root!.Element(StylesName); + if (container is null) { - Requires.NotNull(document, nameof(document)); - Requires.NotNull(name, nameof(name)); + document.Root.Add(container = new XElement(StylesName)); + } - XElement? container = document.Root.Element(name); - if (container is null) - { - document.Root.Add(container = new XElement(name)); - } + var style = new XElement( + StyleName, + new XAttribute("TargetType", targetType), + new XAttribute("GroupLabel", categoryId), + new XElement(XName.Get("Condition", Namespace), new XAttribute("Expression", "HasCategory('" + categoryId + "')"))); + style.Add(properties.Select(p => new XElement(XName.Get("Setter", Namespace), new XAttribute("Property", p.Key), new XAttribute("Value", p.Value!)))); + + container.Add(style); + + return document; + } - return container; + internal static XDocument WithStyle(this XDocument document, string categoryId, string targetType = "Node", string? foreground = null, string? background = null, string? icon = null) + { + var properties = new Dictionary(); + if (!string.IsNullOrEmpty(foreground)) + { + properties.Add("Foreground", foreground); } - private static XElement GetRootElement(XDocument document, string elementName) + if (!string.IsNullOrEmpty(background)) { - Requires.NotNull(document, nameof(document)); - Requires.NotNullOrEmpty(elementName, nameof(elementName)); + properties.Add("Background", background); + } - return GetRootElement(document, XName.Get(elementName, Namespace)); + if (!string.IsNullOrEmpty(icon)) + { + properties.Add("Icon", icon); } + + return WithStyle(document, categoryId, properties, targetType); + } + + private static XElement GetRootElement(this XDocument document, XName name) + { + Requires.NotNull(document, nameof(document)); + Requires.NotNull(name, nameof(name)); + + XElement? container = document.Root!.Element(name); + if (container is null) + { + document.Root.Add(container = new XElement(name)); + } + + return container; + } + + private static XElement GetRootElement(XDocument document, string elementName) + { + Requires.NotNull(document, nameof(document)); + Requires.NotNullOrEmpty(elementName, nameof(elementName)); + + return GetRootElement(document, XName.Get(elementName, Namespace)); } } diff --git a/src/Microsoft.VisualStudio.Threading/DispatcherExtensions.cs b/src/Microsoft.VisualStudio.Threading/DispatcherExtensions.cs index 682f6075f..1b93c4bef 100644 --- a/src/Microsoft.VisualStudio.Threading/DispatcherExtensions.cs +++ b/src/Microsoft.VisualStudio.Threading/DispatcherExtensions.cs @@ -1,74 +1,83 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -#if NETFRAMEWORK +#if NETFRAMEWORK || WINDOWS -namespace Microsoft.VisualStudio.Threading +using System; +using System.Threading; +using System.Windows.Threading; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// Extension methods for the WPF for better +/// interop with the . +/// +public static class DispatcherExtensions { - using System; - using System.Threading; - using System.Windows.Threading; + /// + /// Creates a that schedules work with the specified + /// and . + /// + /// The underlying to use. + /// The that schedules work on the main thread. + /// + /// The priority with which to schedule any work on the UI thread, + /// when and if is called + /// and for each asynchronous return to the main thread after an . + /// + /// A that may be used for scheduling async work with the specified priority. + /// + /// In addition to scheduling work on the UI thread with the specified priority, + /// this also ensures that any synchronous waits + /// on the main thread within objects created with the returned factory + /// will honor calls to , producing similar behavior + /// to . + /// + public static JoinableTaskFactory WithPriority(this JoinableTaskFactory joinableTaskFactory, Dispatcher dispatcher, DispatcherPriority priority) + { + Requires.NotNull(joinableTaskFactory, nameof(joinableTaskFactory)); + Requires.NotNull(dispatcher, nameof(dispatcher)); + + return new DispatcherJoinableTaskFactory(joinableTaskFactory, dispatcher, priority); + } /// - /// Extension methods for the WPF for better - /// interop with the . + /// A that schedules work on the UI thread + /// according to a given . /// - public static class DispatcherExtensions + private class DispatcherJoinableTaskFactory : DelegatingJoinableTaskFactory { /// - /// Creates a that schedules work with the specified - /// and . + /// The to use for scheduling work on the UI thread. /// - /// The underlying to use. - /// The that schedules work on the main thread. - /// - /// The priority with which to schedule any work on the UI thread, - /// when and if is called - /// and for each asynchronous return to the main thread after an await. - /// - /// A that may be used for scheduling async work with the specified priority. - public static JoinableTaskFactory WithPriority(this JoinableTaskFactory joinableTaskFactory, Dispatcher dispatcher, DispatcherPriority priority) - { - Requires.NotNull(joinableTaskFactory, nameof(joinableTaskFactory)); - Requires.NotNull(dispatcher, nameof(dispatcher)); + private readonly Dispatcher dispatcher; - return new DispatcherJoinableTaskFactory(joinableTaskFactory, dispatcher, priority); - } + /// + /// The priority with which to schedule work on the UI thread. + /// + private readonly DispatcherPriority priority; /// - /// A that schedules work on the UI thread - /// according to a given . + /// Initializes a new instance of the class. /// - private class DispatcherJoinableTaskFactory : DelegatingJoinableTaskFactory + /// The underlying to use when scheduling. + /// The to use for scheduling work on the UI thread. + /// The priority with which to schedule work on the UI thread. + internal DispatcherJoinableTaskFactory(JoinableTaskFactory innerFactory, Dispatcher dispatcher, DispatcherPriority priority) + : base(innerFactory) { - /// - /// The to use for scheduling work on the UI thread. - /// - private readonly Dispatcher dispatcher; - - /// - /// The priority with which to schedule work on the UI thread. - /// - private readonly DispatcherPriority priority; - - /// - /// Initializes a new instance of the class. - /// - /// The underlying to use when scheduling. - /// The to use for scheduling work on the UI thread. - /// The priority with which to schedule work on the UI thread. - internal DispatcherJoinableTaskFactory(JoinableTaskFactory innerFactory, Dispatcher dispatcher, DispatcherPriority priority) - : base(innerFactory) - { - this.dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); - this.priority = priority; - } + this.dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); +#if NETFRAMEWORK // Avoid trim warnings on .NET, and only .NET Framework calls CoWait anyway. + this.DefaultWaitPolicy = new DispatcherSynchronizationContext(dispatcher); +#endif + this.priority = priority; + } - /// - protected internal override void PostToUnderlyingSynchronizationContext(SendOrPostCallback callback, object state) - { - this.dispatcher.BeginInvoke(this.priority, callback, state); - } + /// + protected internal override void PostToUnderlyingSynchronizationContext(SendOrPostCallback callback, object state) + { + this.dispatcher.BeginInvoke(this.priority, callback, state); } } } diff --git a/src/Microsoft.VisualStudio.Threading/EmptyStruct.cs b/src/Microsoft.VisualStudio.Threading/EmptyStruct.cs index 2e16ca387..2a08dbf21 100644 --- a/src/Microsoft.VisualStudio.Threading/EmptyStruct.cs +++ b/src/Microsoft.VisualStudio.Threading/EmptyStruct.cs @@ -1,22 +1,21 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +namespace Microsoft.VisualStudio.Threading; + +/// +/// An empty struct. +/// +/// +/// This can save 4 bytes over System.Object when a type argument is required for a generic type, but entirely unused. +/// +internal readonly struct EmptyStruct { /// - /// An empty struct. + /// Gets an instance of the empty struct. /// - /// - /// This can save 4 bytes over System.Object when a type argument is required for a generic type, but entirely unused. - /// - internal readonly struct EmptyStruct + internal static EmptyStruct Instance { - /// - /// Gets an instance of the empty struct. - /// - internal static EmptyStruct Instance - { - get { return default(EmptyStruct); } - } + get { return default(EmptyStruct); } } } diff --git a/src/Microsoft.VisualStudio.Threading/EnumerateOneOrMany`1.cs b/src/Microsoft.VisualStudio.Threading/EnumerateOneOrMany`1.cs index bd898c3b8..4a1f12e48 100644 --- a/src/Microsoft.VisualStudio.Threading/EnumerateOneOrMany`1.cs +++ b/src/Microsoft.VisualStudio.Threading/EnumerateOneOrMany`1.cs @@ -1,148 +1,147 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading -{ - using System; - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - using System.Linq; - using System.Text; - using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; +/// +/// Enumerates either a single element or a list of elements. +/// +/// The type of element to enumerate. +internal struct EnumerateOneOrMany : IEnumerator +{ /// - /// Enumerates either a single element or a list of elements. + /// The single element to enumerate, when applicable. /// - /// The type of element to enumerate. - internal struct EnumerateOneOrMany : IEnumerator - { - /// - /// The single element to enumerate, when applicable. - /// - [AllowNull, MaybeNull] - private T value; + [AllowNull, MaybeNull] + private T value; - /// - /// The enumerator of the list. - /// - private List.Enumerator enumerator; + /// + /// The enumerator of the list. + /// + private List.Enumerator enumerator; - /// - /// A value indicating whether a single element or a list of them is being enumerated. - /// - private bool justOne; + /// + /// A value indicating whether a single element or a list of them is being enumerated. + /// + private bool justOne; - /// - /// The position around the lone element being enumerated, when applicable. - /// - private int position; + /// + /// The position around the lone element being enumerated, when applicable. + /// + private int position; - /// - /// Initializes a new instance of the struct. - /// - /// The single value to enumerate. - internal EnumerateOneOrMany(T value) - { - this.value = value; - this.enumerator = default(List.Enumerator); - this.justOne = true; - this.position = -1; - } + /// + /// Initializes a new instance of the struct. + /// + /// The single value to enumerate. + internal EnumerateOneOrMany(T value) + { + this.value = value; + this.enumerator = default(List.Enumerator); + this.justOne = true; + this.position = -1; + } - /// - /// Initializes a new instance of the struct. - /// - /// The list of values to enumerate. - internal EnumerateOneOrMany(List values) - { - this.value = default(T)!; - this.enumerator = values.GetEnumerator(); - this.justOne = false; - this.position = 0; // N/A - } + /// + /// Initializes a new instance of the struct. + /// + /// The list of values to enumerate. + internal EnumerateOneOrMany(List values) + { + this.value = default(T)!; + this.enumerator = values.GetEnumerator(); + this.justOne = false; + this.position = 0; // N/A + } - /// - /// Gets the current value. - /// - public T Current + /// + /// Gets the current value. + /// + public T Current + { + get { - get + if (this.justOne) { - if (this.justOne) + if (this.position == 0) { - if (this.position == 0) - { - return this.value!; - } - else - { - throw new InvalidOperationException(); - } + return this.value!; } else { - return this.enumerator.Current; + throw new InvalidOperationException(); } } + else + { + return this.enumerator.Current; + } } + } - /// - /// Gets the current value. - /// - object? System.Collections.IEnumerator.Current - { - get { return this.Current; } - } + /// + /// Gets the current value. + /// + object? System.Collections.IEnumerator.Current + { + get { return this.Current; } + } - /// - /// Disposes this enumerator. - /// - public void Dispose() - { - this.enumerator.Dispose(); - } + /// + /// Disposes this enumerator. + /// + public void Dispose() + { + this.enumerator.Dispose(); + } - /// - /// Advances enumeration to the next element. - /// - public bool MoveNext() + /// + /// Advances enumeration to the next element. + /// + public bool MoveNext() + { + if (this.justOne) { - if (this.justOne) + if (this.position == -1) { - if (this.position == -1) - { - this.position = 0; - return true; - } - else if (this.position == 0) - { - this.position++; - return false; - } - else - { - return false; - } + this.position = 0; + return true; + } + else if (this.position == 0) + { + this.position++; + return false; } else { - return this.enumerator.MoveNext(); + return false; } } + else + { + return this.enumerator.MoveNext(); + } + } - /// - /// Resets this enumerator. - /// - void System.Collections.IEnumerator.Reset() + /// + /// Resets this enumerator. + /// + void System.Collections.IEnumerator.Reset() + { + if (this.justOne) { - if (this.justOne) - { - this.position = -1; - } - else - { - ((System.Collections.IEnumerator)this.enumerator).Reset(); - } + this.position = -1; + } + else + { + ((System.Collections.IEnumerator)this.enumerator).Reset(); } } } diff --git a/src/Microsoft.VisualStudio.Threading/GlobalSuppressions.cs b/src/Microsoft.VisualStudio.Threading/GlobalSuppressions.cs index ad548396c..d4ff22a8f 100644 Binary files a/src/Microsoft.VisualStudio.Threading/GlobalSuppressions.cs and b/src/Microsoft.VisualStudio.Threading/GlobalSuppressions.cs differ diff --git a/src/Microsoft.VisualStudio.Threading/HangReportContribution.cs b/src/Microsoft.VisualStudio.Threading/HangReportContribution.cs index 5a7ee3026..d017ea7db 100644 --- a/src/Microsoft.VisualStudio.Threading/HangReportContribution.cs +++ b/src/Microsoft.VisualStudio.Threading/HangReportContribution.cs @@ -1,66 +1,65 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Text; - using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; +/// +/// A contribution to an aggregate hang report. +/// +public class HangReportContribution +{ /// - /// A contribution to an aggregate hang report. + /// Initializes a new instance of the class. /// - public class HangReportContribution + /// The content for the hang report. + /// The MIME type of the attached content. + /// The suggested filename of the content when it is attached in a report. + public HangReportContribution(string content, string? contentType, string? contentName) { - /// - /// Initializes a new instance of the class. - /// - /// The content for the hang report. - /// The MIME type of the attached content. - /// The suggested filename of the content when it is attached in a report. - public HangReportContribution(string content, string? contentType, string? contentName) - { - Requires.NotNull(content, nameof(content)); - this.Content = content; - this.ContentType = contentType; - this.ContentName = contentName; - } + Requires.NotNull(content, nameof(content)); + this.Content = content; + this.ContentType = contentType; + this.ContentName = contentName; + } - /// - /// Initializes a new instance of the class. - /// - /// The content for the hang report. - /// The MIME type of the attached content. - /// The suggested filename of the content when it is attached in a report. - /// Nested reports. - public HangReportContribution(string content, string? contentType, string? contentName, params HangReportContribution[]? nestedReports) - : this(content, contentType, contentName) - { - this.NestedReports = nestedReports; - } + /// + /// Initializes a new instance of the class. + /// + /// The content for the hang report. + /// The MIME type of the attached content. + /// The suggested filename of the content when it is attached in a report. + /// Nested reports. + public HangReportContribution(string content, string? contentType, string? contentName, params HangReportContribution[]? nestedReports) + : this(content, contentType, contentName) + { + this.NestedReports = nestedReports; + } - /// - /// Gets the content of the hang report. - /// - public string Content { get; private set; } + /// + /// Gets the content of the hang report. + /// + public string Content { get; private set; } - /// - /// Gets the MIME type for the content. - /// - public string? ContentType { get; private set; } + /// + /// Gets the MIME type for the content. + /// + public string? ContentType { get; private set; } - /// - /// Gets the suggested filename for the content. - /// - public string? ContentName { get; private set; } + /// + /// Gets the suggested filename for the content. + /// + public string? ContentName { get; private set; } - /// - /// Gets the nested hang reports, if any. - /// - /// A read only collection, or null. - public IReadOnlyCollection? NestedReports { get; private set; } - } + /// + /// Gets the nested hang reports, if any. + /// + /// A read only collection, or . + public IReadOnlyCollection? NestedReports { get; private set; } } diff --git a/src/Microsoft.VisualStudio.Threading/IAsyncDisposable.cs b/src/Microsoft.VisualStudio.Threading/IAsyncDisposable.cs index 867c595e2..189ba04d1 100644 --- a/src/Microsoft.VisualStudio.Threading/IAsyncDisposable.cs +++ b/src/Microsoft.VisualStudio.Threading/IAsyncDisposable.cs @@ -1,22 +1,21 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading -{ - using System.Threading.Tasks; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; +/// +/// Defines an asynchronous method to release allocated resources. +/// +/// +/// Consider implementing instead. +/// +public interface IAsyncDisposable +{ /// - /// Defines an asynchronous method to release allocated resources. + /// Performs application-defined tasks associated with freeing, + /// releasing, or resetting unmanaged resources asynchronously. /// - /// - /// Consider implementing instead. - /// - public interface IAsyncDisposable - { - /// - /// Performs application-defined tasks associated with freeing, - /// releasing, or resetting unmanaged resources asynchronously. - /// - Task DisposeAsync(); - } + Task DisposeAsync(); } diff --git a/src/Microsoft.VisualStudio.Threading/IHangReportContributor.cs b/src/Microsoft.VisualStudio.Threading/IHangReportContributor.cs index 15821c6e4..7b5cac146 100644 --- a/src/Microsoft.VisualStudio.Threading/IHangReportContributor.cs +++ b/src/Microsoft.VisualStudio.Threading/IHangReportContributor.cs @@ -1,23 +1,19 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Text; - using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.VisualStudio.Threading; +/// +/// Provides a facility to produce reports that may be useful when analyzing hangs. +/// +public interface IHangReportContributor +{ /// - /// Provides a facility to produce reports that may be useful when analyzing hangs. + /// Contributes data for a hang report. /// - public interface IHangReportContributor - { - /// - /// Contributes data for a hang report. - /// - /// The hang report contribution. Null values should be ignored. - HangReportContribution GetHangReport(); - } + /// The hang report contribution. Null values should be ignored. + [RequiresUnreferencedCode(Reasons.DiagnosticAnalysisOnly)] + HangReportContribution GetHangReport(); } diff --git a/src/Microsoft.VisualStudio.Threading/IJoinableTaskDependent.cs b/src/Microsoft.VisualStudio.Threading/IJoinableTaskDependent.cs index dc80229c8..5371cdaa2 100644 --- a/src/Microsoft.VisualStudio.Threading/IJoinableTaskDependent.cs +++ b/src/Microsoft.VisualStudio.Threading/IJoinableTaskDependent.cs @@ -1,46 +1,45 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +namespace Microsoft.VisualStudio.Threading; + +/// +/// Represents a dependent item in the JoinableTask dependency graph, it can be either a or a . +/// +internal interface IJoinableTaskDependent { /// - /// Represents a dependent item in the JoinableTask dependency graph, it can be either a or a . + /// Gets the this node belongs to. + /// + JoinableTaskContext JoinableTaskContext { get; } + + /// + /// Gets a value indicating whether we need reference count child dependent node. This is to keep the current behavior of . + /// + bool NeedRefCountChildDependencies { get; } + + /// + /// Get the reference of dependent node to record dependencies. + /// + ref JoinableTaskDependencyGraph.JoinableTaskDependentData GetJoinableTaskDependentData(); + + /// + /// A function is called, when this dependent node is added to be a dependency of a parent node. + /// + void OnAddedToDependency(IJoinableTaskDependent parent); + + /// + /// A function is called, when this dependent node is removed as a dependency of a parent node. + /// + void OnRemovedFromDependency(IJoinableTaskDependent parentNode); + + /// + /// A function is called, when a dependent child is added. + /// + void OnDependencyAdded(IJoinableTaskDependent joinChild); + + /// + /// A function is called, when a dependent child is removed. /// - internal interface IJoinableTaskDependent - { - /// - /// Gets the this node belongs to. - /// - JoinableTaskContext JoinableTaskContext { get; } - - /// - /// Gets a value indicating whether we need reference count child dependent node. This is to keep the current behavior of . - /// - bool NeedRefCountChildDependencies { get; } - - /// - /// Get the reference of dependent node to record dependencies. - /// - ref JoinableTaskDependencyGraph.JoinableTaskDependentData GetJoinableTaskDependentData(); - - /// - /// A function is called, when this dependent node is added to be a dependency of a parent node. - /// - void OnAddedToDependency(IJoinableTaskDependent parent); - - /// - /// A function is called, when this dependent node is removed as a dependency of a parent node. - /// - void OnRemovedFromDependency(IJoinableTaskDependent parentNode); - - /// - /// A function is called, when a dependent child is added. - /// - void OnDependencyAdded(IJoinableTaskDependent joinChild); - - /// - /// A function is called, when a dependent child is removed. - /// - void OnDependencyRemoved(IJoinableTaskDependent joinChild); - } + void OnDependencyRemoved(IJoinableTaskDependent joinChild); } diff --git a/src/Microsoft.VisualStudio.Threading/IPendingExecutionRequestState.cs b/src/Microsoft.VisualStudio.Threading/IPendingExecutionRequestState.cs new file mode 100644 index 000000000..9d73c4e8c --- /dev/null +++ b/src/Microsoft.VisualStudio.Threading/IPendingExecutionRequestState.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.VisualStudio.Threading +{ + /// + /// An optional interface implemented by pending request state posted to the underline synchronization context. It allows synchronization context to remove completed requests. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Experimental("VSOnly")] + public interface IPendingExecutionRequestState + { + /// + /// Gets a value indicating whether the current request has been completed, and can be skipped. + /// + bool IsCompleted { get; } + } +} diff --git a/src/Microsoft.VisualStudio.Threading/IllegalSemaphoreUsageException.cs b/src/Microsoft.VisualStudio.Threading/IllegalSemaphoreUsageException.cs index 1660c1905..3d52226b7 100644 --- a/src/Microsoft.VisualStudio.Threading/IllegalSemaphoreUsageException.cs +++ b/src/Microsoft.VisualStudio.Threading/IllegalSemaphoreUsageException.cs @@ -1,22 +1,21 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading -{ - using System; - using System.Globalization; +using System; +using System.Globalization; + +namespace Microsoft.VisualStudio.Threading; +/// +/// Exception which is thrown when the contract of a is violated. +/// +public class IllegalSemaphoreUsageException : InvalidOperationException +{ /// - /// Exception which is thrown when the contract of a is violated. + /// Initializes a new instance of the class. /// - public class IllegalSemaphoreUsageException : InvalidOperationException + public IllegalSemaphoreUsageException(string message) + : base(message) { - /// - /// Initializes a new instance of the class. - /// - public IllegalSemaphoreUsageException(string message) - : base(message) - { - } } } diff --git a/src/Microsoft.VisualStudio.Threading/InlineResumable.cs b/src/Microsoft.VisualStudio.Threading/InlineResumable.cs index 76166dbec..2a7f83013 100644 --- a/src/Microsoft.VisualStudio.Threading/InlineResumable.cs +++ b/src/Microsoft.VisualStudio.Threading/InlineResumable.cs @@ -1,90 +1,89 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading -{ - using System; - using System.Runtime.CompilerServices; - using System.Threading; +using System; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace Microsoft.VisualStudio.Threading; +/// +/// An awaiter that can be pre-created, and later immediately execute its one scheduled continuation. +/// +internal class InlineResumable : ICriticalNotifyCompletion +{ /// - /// An awaiter that can be pre-created, and later immediately execute its one scheduled continuation. + /// The continuation that has been scheduled. /// - internal class InlineResumable : ICriticalNotifyCompletion - { - /// - /// The continuation that has been scheduled. - /// - private Action? continuation; + private Action? continuation; - /// - /// The current as of when the continuation was scheduled. - /// - private SynchronizationContext? capturedSynchronizationContext; + /// + /// The current as of when the continuation was scheduled. + /// + private SynchronizationContext? capturedSynchronizationContext; - /// - /// Whether has been called already. - /// - private bool resumed; + /// + /// Whether has been called already. + /// + private bool resumed; - /// - /// Gets a value indicating whether an awaiting expression should yield. - /// - /// Always false. - public bool IsCompleted => this.resumed; + /// + /// Gets a value indicating whether an awaiting expression should yield. + /// + /// Always . + public bool IsCompleted => this.resumed; - /// - /// Does and returns nothing. - /// - public void GetResult() - { - } + /// + /// Does and returns nothing. + /// + public void GetResult() + { + } - /// - /// Stores the continuation for later execution when is invoked. - /// - /// The delegate to execute later. - public void OnCompleted(Action continuation) - { - Requires.NotNull(continuation, nameof(continuation)); - Assumes.Null(this.continuation); // Only one continuation is supported. + /// + /// Stores the continuation for later execution when is invoked. + /// + /// The delegate to execute later. + public void OnCompleted(Action continuation) + { + Requires.NotNull(continuation, nameof(continuation)); + Assumes.Null(this.continuation); // Only one continuation is supported. - this.capturedSynchronizationContext = SynchronizationContext.Current; - this.continuation = continuation; - } + this.capturedSynchronizationContext = SynchronizationContext.Current; + this.continuation = continuation; + } - /// - /// Stores the continuation for later execution when is invoked. - /// - /// The delegate to execute later. - public void UnsafeOnCompleted(Action continuation) - { - // We don't capture ExecutionContext even in the normal path - // as this is a very special case and internal awaiter. - // Strictly speaking, we don't have to implement ICriticalNotifyCompletion, - // but by implementing it, we show that we don't capture context and avoid - // code audits later from spending time asking why this awaiter isn't so optimized. - this.OnCompleted(continuation); - } + /// + /// Stores the continuation for later execution when is invoked. + /// + /// The delegate to execute later. + public void UnsafeOnCompleted(Action continuation) + { + // We don't capture ExecutionContext even in the normal path + // as this is a very special case and internal awaiter. + // Strictly speaking, we don't have to implement ICriticalNotifyCompletion, + // but by implementing it, we show that we don't capture context and avoid + // code audits later from spending time asking why this awaiter isn't so optimized. + this.OnCompleted(continuation); + } - /// - /// Gets this instance. This method makes this awaiter double as its own awaitable. - /// - /// This instance. - public InlineResumable GetAwaiter() => this; + /// + /// Gets this instance. This method makes this awaiter double as its own awaitable. + /// + /// This instance. + public InlineResumable GetAwaiter() => this; - /// - /// Executes the continuation immediately, on the caller's thread. - /// - public void Resume() + /// + /// Executes the continuation immediately, on the caller's thread. + /// + public void Resume() + { + this.resumed = true; + Action? continuation = this.continuation; + this.continuation = null; + using (this.capturedSynchronizationContext.Apply()) { - this.resumed = true; - Action? continuation = this.continuation; - this.continuation = null; - using (this.capturedSynchronizationContext.Apply()) - { - continuation?.Invoke(); - } + continuation?.Invoke(); } } } diff --git a/src/Microsoft.VisualStudio.Threading/InternalUtilities.cs b/src/Microsoft.VisualStudio.Threading/InternalUtilities.cs index bd11ed3c2..f9eaba00a 100644 --- a/src/Microsoft.VisualStudio.Threading/InternalUtilities.cs +++ b/src/Microsoft.VisualStudio.Threading/InternalUtilities.cs @@ -1,289 +1,47 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading -{ - using System; - using System.Collections; - using System.Collections.Generic; - using System.Globalization; - using System.Linq; - using System.Reflection; - using System.Runtime.CompilerServices; - using System.Threading.Tasks; +using System.Collections.Generic; + +namespace Microsoft.VisualStudio.Threading; +/// +/// Internal helper/extension methods for this assembly's own use. +/// +internal static class InternalUtilities +{ /// - /// Internal helper/extension methods for this assembly's own use. + /// Removes an element from the middle of a queue without disrupting the other elements. /// - internal static class InternalUtilities + /// The element to remove. + /// The queue to modify. + /// The value to remove. + /// + /// If a value appears multiple times in the queue, only its first entry is removed. + /// + internal static bool RemoveMidQueue(this Queue queue, T valueToRemove) + where T : class { - /// - /// The substring that should be inserted before each async return stack frame. - /// - /// - /// When printing synchronous callstacks, .NET begins each frame with " at ". - /// When printing async return stack, we use this to indicate continuations. - /// - private const string AsyncReturnStackPrefix = " -> "; - - /// - /// Removes an element from the middle of a queue without disrupting the other elements. - /// - /// The element to remove. - /// The queue to modify. - /// The value to remove. - /// - /// If a value appears multiple times in the queue, only its first entry is removed. - /// - internal static bool RemoveMidQueue(this Queue queue, T valueToRemove) - where T : class - { - Requires.NotNull(queue, nameof(queue)); - Requires.NotNull(valueToRemove, nameof(valueToRemove)); - - int originalCount = queue.Count; - int dequeueCounter = 0; - bool found = false; - while (dequeueCounter < originalCount) - { - dequeueCounter++; - T dequeued = queue.Dequeue(); - if (!found && dequeued == valueToRemove) - { // only find 1 match - found = true; - } - else - { - queue.Enqueue(dequeued); - } - } - - return found; - } - - /// - /// Walk the continuation objects inside "async state machines" to generate the return callstack. - /// FOR DIAGNOSTIC PURPOSES ONLY. - /// - /// The delegate that represents the head of an async continuation chain. - internal static IEnumerable GetAsyncReturnStackFrames(this Delegate continuationDelegate) - { - IAsyncStateMachine? stateMachine = FindAsyncStateMachine(continuationDelegate); - if (stateMachine is null) - { - // Did not find the async state machine, so returns the method name as top frame and stop walking. - yield return GetDelegateLabel(continuationDelegate); - yield break; - } - - do - { - var state = GetStateMachineFieldValueOnSuffix(stateMachine, "__state"); - yield return string.Format( - CultureInfo.CurrentCulture, - "{2}{0} (state: {1}, address: 0x{3:X8})", - stateMachine.GetType().FullName, - state, - AsyncReturnStackPrefix, - (int)GetAddress(stateMachine)); // the int cast allows hex formatting - - Delegate[]? continuationDelegates = FindContinuationDelegates(stateMachine).ToArray(); - if (continuationDelegates.Length == 0) - { - break; - } + Requires.NotNull(queue, nameof(queue)); + Requires.NotNull(valueToRemove, nameof(valueToRemove)); - // Consider: It's possible but uncommon scenario to have multiple "async methods" being awaiting for one "async method". - // Here we just choose the first awaiting "async method" as that should be good enough for postmortem. - // In future we might want to revisit this to cover the other awaiting "async methods". - stateMachine = continuationDelegates.Select((d) => FindAsyncStateMachine(d)) - .FirstOrDefault((s) => s is object); - if (stateMachine is null) - { - yield return GetDelegateLabel(continuationDelegates.First()); - } - } - while (stateMachine is object); - } - - /// - /// A helper method to get the label of the given delegate. - /// - private static string GetDelegateLabel(Delegate invokeDelegate) + int originalCount = queue.Count; + int dequeueCounter = 0; + bool found = false; + while (dequeueCounter < originalCount) { - Requires.NotNull(invokeDelegate, nameof(invokeDelegate)); - - MethodInfo? method = invokeDelegate.GetMethodInfo(); - if (invokeDelegate.Target is object) - { - string instanceType = string.Empty; - if (!(method?.DeclaringType?.Equals(invokeDelegate.Target.GetType()) ?? false)) - { - instanceType = " (" + invokeDelegate.Target.GetType().FullName + ")"; - } - - return string.Format( - CultureInfo.CurrentCulture, - "{3}{0}.{1}{2} (target address: 0x{4:X" + (IntPtr.Size * 2) + "})", - method?.DeclaringType?.FullName, - method?.Name, - instanceType, - AsyncReturnStackPrefix, - GetAddress(invokeDelegate.Target).ToInt64()); // the cast allows hex formatting - } - - return string.Format( - CultureInfo.CurrentCulture, - "{2}{0}.{1}", - method?.DeclaringType?.FullName, - method?.Name, - AsyncReturnStackPrefix); - } - - /// - /// Gets the memory address of a given object. - /// - /// The object to get the address for. - /// The memory address. - /// - /// This method works when GCHandle will refuse because the type of object is a non-blittable type. - /// However, this method provides no guarantees that the address will remain valid for the caller, - /// so it is only useful for diagnostics and when we don't expect addresses to be changing much any more. - /// - private static unsafe IntPtr GetAddress(object value) => new IntPtr(Unsafe.AsPointer(ref value)); - - /// - /// A helper method to find the async state machine from the given delegate. - /// - private static IAsyncStateMachine? FindAsyncStateMachine(Delegate invokeDelegate) - { - Requires.NotNull(invokeDelegate, nameof(invokeDelegate)); - - if (invokeDelegate.Target is object) - { - // Some delegates are wrapped with a ContinuationWrapper object. We have to unwrap that in those cases. - // In testing, this m_continuation field jump is only required when the debugger is attached -- weird. - // I suspect however that it's a natural behavior of the async state machine (when there are >1 continuations perhaps). - // So we check for the case in all cases. - if (GetFieldValue(invokeDelegate.Target, "m_continuation") is Action continuation) - { - invokeDelegate = continuation; - if (invokeDelegate.Target is null) - { - return null; - } - } - - var stateMachine = GetFieldValue(invokeDelegate.Target, "m_stateMachine") as IAsyncStateMachine; - return stateMachine; - } - - return null; - } - - /// - /// This is the core to find the continuation delegate(s) inside the given async state machine. - /// The chain of objects is like this: async state machine -> async method builder -> task -> continuation object -> action. - /// - /// - /// There are 3 types of "async method builder": AsyncVoidMethodBuilder, AsyncTaskMethodBuilder, AsyncTaskMethodBuilder<T>. - /// We don't cover AsyncVoidMethodBuilder as it is used rarely and it can't be awaited either; - /// AsyncTaskMethodBuilder is a wrapper on top of AsyncTaskMethodBuilder<VoidTaskResult>. - /// - private static IEnumerable FindContinuationDelegates(IAsyncStateMachine stateMachine) - { - Requires.NotNull(stateMachine, nameof(stateMachine)); - - var builder = GetStateMachineFieldValueOnSuffix(stateMachine, "__builder"); - if (builder is null) - { - yield break; - } - - var task = GetFieldValue(builder, "m_task"); - if (task is null) - { - // Probably this builder is an instance of "AsyncTaskMethodBuilder", so we need to get its inner "AsyncTaskMethodBuilder" - builder = GetFieldValue(builder, "m_builder"); - if (builder is object) - { - task = GetFieldValue(builder, "m_task"); - } - } - - if (task is null) - { - yield break; - } - - // "task" might be an instance of the type deriving from "Task", but "m_continuationObject" is a private field in "Task", - // so we need to use "typeof(Task)" to access "m_continuationObject". - FieldInfo? continuationField = typeof(Task).GetTypeInfo().GetDeclaredField("m_continuationObject"); - if (continuationField is null) - { - yield break; - } - - var continuationObject = continuationField.GetValue(task); - if (continuationObject is null) - { - yield break; - } - - if (continuationObject is IEnumerable items) - { - foreach (var item in items) - { - Delegate? action = item as Delegate ?? GetFieldValue(item!, "m_action") as Delegate; - if (action is object) - { - yield return action; - } - } + dequeueCounter++; + T dequeued = queue.Dequeue(); + if (!found && dequeued == valueToRemove) + { // only find 1 match + found = true; } else { - Delegate? action = continuationObject as Delegate ?? GetFieldValue(continuationObject, "m_action") as Delegate; - if (action is object) - { - yield return action; - } - } - } - - /// - /// A helper method to get field's value given the object and the field name. - /// - private static object? GetFieldValue(object obj, string fieldName) - { - Requires.NotNull(obj, nameof(obj)); - Requires.NotNullOrEmpty(fieldName, nameof(fieldName)); - - FieldInfo? field = obj.GetType().GetTypeInfo().GetDeclaredField(fieldName); - if (field is object) - { - return field.GetValue(obj); + queue.Enqueue(dequeued); } - - return null; } - /// - /// The field names of "async state machine" are not fixed; the workaround is to find the field based on the suffix. - /// - private static object? GetStateMachineFieldValueOnSuffix(IAsyncStateMachine stateMachine, string suffix) - { - Requires.NotNull(stateMachine, nameof(stateMachine)); - Requires.NotNullOrEmpty(suffix, nameof(suffix)); - - IEnumerable? fields = stateMachine.GetType().GetTypeInfo().DeclaredFields; - FieldInfo? field = fields.FirstOrDefault((f) => f.Name.EndsWith(suffix, StringComparison.Ordinal)); - if (field is object) - { - return field.GetValue(stateMachine); - } - - return null; - } + return found; } } diff --git a/src/Microsoft.VisualStudio.Threading/JoinableTask+ExecutionQueue.cs b/src/Microsoft.VisualStudio.Threading/JoinableTask+ExecutionQueue.cs index a765d3d66..097159a60 100644 --- a/src/Microsoft.VisualStudio.Threading/JoinableTask+ExecutionQueue.cs +++ b/src/Microsoft.VisualStudio.Threading/JoinableTask+ExecutionQueue.cs @@ -1,83 +1,91 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Text; - using System.Threading.Tasks; - using SingleExecuteProtector = Microsoft.VisualStudio.Threading.JoinableTaskFactory.SingleExecuteProtector; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SingleExecuteProtector = Microsoft.VisualStudio.Threading.JoinableTaskFactory.SingleExecuteProtector; + +namespace Microsoft.VisualStudio.Threading; - public partial class JoinableTask +public partial class JoinableTask +{ + /// + /// A thread-safe queue of elements + /// that self-scavenges elements that are executed by other means. + /// + internal class ExecutionQueue : AsyncQueue { - /// - /// A thread-safe queue of elements - /// that self-scavenges elements that are executed by other means. - /// - internal class ExecutionQueue : AsyncQueue + private readonly JoinableTask owningJob; + + internal ExecutionQueue(JoinableTask owningJob) { - private readonly JoinableTask owningJob; + Requires.NotNull(owningJob, nameof(owningJob)); + this.owningJob = owningJob; + } - internal ExecutionQueue(JoinableTask owningJob) - { - Requires.NotNull(owningJob, nameof(owningJob)); - this.owningJob = owningJob; - } + protected override int InitialCapacity + { + get { return 1; } // in non-concurrent cases, 1 is sufficient. + } - protected override int InitialCapacity - { - get { return 1; } // in non-concurrent cases, 1 is sufficient. - } + internal void OnExecuting(object sender, EventArgs e) + { + this.Scavenge(); + } - internal void OnExecuting(object sender, EventArgs e) - { - this.Scavenge(); - } + protected override void OnEnqueued(SingleExecuteProtector value, bool alreadyDispatched) + { + base.OnEnqueued(value, alreadyDispatched); - protected override void OnEnqueued(SingleExecuteProtector value, bool alreadyDispatched) + // We only need to consider scavenging our queue if this item was + // actually added to the queue. + if (!alreadyDispatched) { - base.OnEnqueued(value, alreadyDispatched); + Requires.NotNull(value, nameof(value)); + value.AddExecutingCallback(this); - // We only need to consider scavenging our queue if this item was - // actually added to the queue. - if (!alreadyDispatched) + // It's possible this value has already been executed + // (before our event wire-up was applied). So check and + // scavenge. + if (value.HasBeenExecuted) { - Requires.NotNull(value, nameof(value)); - value.AddExecutingCallback(this); - - // It's possible this value has already been executed - // (before our event wire-up was applied). So check and - // scavenge. - if (value.HasBeenExecuted) - { - this.Scavenge(); - } + this.Scavenge(); } } + } - protected override void OnDequeued(SingleExecuteProtector value) - { - Requires.NotNull(value, nameof(value)); - - base.OnDequeued(value); - value.RemoveExecutingCallback(this); - } + protected override void OnDequeued(SingleExecuteProtector value) + { + Requires.NotNull(value, nameof(value)); - protected override void OnCompleted() - { - base.OnCompleted(); + base.OnDequeued(value); + value.RemoveExecutingCallback(this); + } - this.owningJob.OnQueueCompleted(); - } + protected override void OnCompleted() + { + base.OnCompleted(); - private void Scavenge() + if ((this.owningJob.state & JoinableTaskFlags.CompleteFinalized) != JoinableTaskFlags.CompleteFinalized) { - while (this.TryDequeue(p => p.HasBeenExecuted, out SingleExecuteProtector? stale)) + using (this.owningJob.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) { + lock (this.owningJob.JoinableTaskContext.SyncContextLock) + { + this.owningJob.OnQueueCompleted(); + } } } } + + private void Scavenge() + { + while (this.TryDequeue(p => p.HasBeenExecuted, out SingleExecuteProtector? stale)) + { + } + } } } diff --git a/src/Microsoft.VisualStudio.Threading/JoinableTask+JoinableTaskSynchronizationContext.cs b/src/Microsoft.VisualStudio.Threading/JoinableTask+JoinableTaskSynchronizationContext.cs index 905def911..00432f5ae 100644 --- a/src/Microsoft.VisualStudio.Threading/JoinableTask+JoinableTaskSynchronizationContext.cs +++ b/src/Microsoft.VisualStudio.Threading/JoinableTask+JoinableTaskSynchronizationContext.cs @@ -1,15 +1,15 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Windows.Win32; +using Windows.Win32.Foundation; + namespace Microsoft.VisualStudio.Threading { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - public partial class JoinableTask { /// @@ -44,6 +44,11 @@ internal JoinableTaskSynchronizationContext(JoinableTaskFactory owner) this.jobFactory = owner; this.mainThreadAffinitized = true; + + if (owner.DefaultWaitPolicy is not null) + { + this.SetWaitNotificationRequired(); + } } /// @@ -56,6 +61,11 @@ internal JoinableTaskSynchronizationContext(JoinableTask joinableTask, bool main { this.job = joinableTask; this.mainThreadAffinitized = mainThreadAffinitized; + + if (joinableTask.DisableProcessing > 0) + { + this.SetWaitNotificationRequired(); + } } /// @@ -134,6 +144,50 @@ public override void Send(SendOrPostCallback d, object? state) } } + /// + /// Synchronously blocks without a message pump. + /// + /// An array of type that contains the native operating system handles. + /// true to wait for all handles; false to wait for any handle. + /// The number of milliseconds to wait, or (-1) to wait indefinitely. + /// + /// The array index of the object that satisfied the wait. + /// + public override unsafe int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout) + { + Requires.NotNull(waitHandles, nameof(waitHandles)); + + if (this.job?.DisableProcessing > 0) + { + // On .NET Framework we must take special care to NOT end up in a call to CoWait (which lets in RPC calls). + // Off Windows, we can't p/invoke to kernel32, but it appears that .NET never calls CoWait, so we can rely on default behavior. + // We're just going to use the OS as the switch instead of the runtime so that (one day) if we drop our .NET Framework specific target, + // and if .NET ever adds CoWait support on Windows, we'll still behave properly. +#if NET + if (OperatingSystem.IsWindowsVersionAtLeast(5, 1, 2600)) +#else + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) +#endif + { + fixed (IntPtr* pHandles = waitHandles) + { + return (int)PInvoke.WaitForMultipleObjects((uint)waitHandles.Length, (HANDLE*)pHandles, waitAll, (uint)millisecondsTimeout); + } + } + } + + // Use a surrogate default policy if provided. + if (this.jobFactory.DefaultWaitPolicy is { } waitPolicy) + { + return waitPolicy.Wait(waitHandles, waitAll, millisecondsTimeout); + } + + // Fallback to sync blocking such that CoWait might be called. + return WaitHelper(waitHandles, waitAll, millisecondsTimeout); + } + + internal void ConsiderDisableProcessing() => this.SetWaitNotificationRequired(); + /// /// Called by the joinable task when it has completed. /// diff --git a/src/Microsoft.VisualStudio.Threading/JoinableTask.cs b/src/Microsoft.VisualStudio.Threading/JoinableTask.cs index e000a2454..2cf498fdc 100644 --- a/src/Microsoft.VisualStudio.Threading/JoinableTask.cs +++ b/src/Microsoft.VisualStudio.Threading/JoinableTask.cs @@ -1,1209 +1,1379 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using JoinRelease = Microsoft.VisualStudio.Threading.JoinableTaskCollection.JoinRelease; +using SingleExecuteProtector = Microsoft.VisualStudio.Threading.JoinableTaskFactory.SingleExecuteProtector; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// Tracks asynchronous operations and provides the ability to Join those operations to avoid +/// deadlocks while synchronously blocking the Main thread for the operation's completion. +/// +/// +/// For more complete comments please see the . +/// +[DebuggerDisplay("IsCompleted: {IsCompleted}, Method = {EntryMethodInfo != null ? EntryMethodInfo.Name : null}")] +public partial class JoinableTask : IJoinableTaskDependent { - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Diagnostics.CodeAnalysis; - using System.Linq; - using System.Reflection; - using System.Runtime.CompilerServices; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - using JoinRelease = Microsoft.VisualStudio.Threading.JoinableTaskCollection.JoinRelease; - using SingleExecuteProtector = Microsoft.VisualStudio.Threading.JoinableTaskFactory.SingleExecuteProtector; + /// + /// Stores the top-most JoinableTask that is completing on the current thread, if any. + /// + private static readonly ThreadLocal CompletingTask = new ThreadLocal(); + + /// + /// The that began the async operation. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly JoinableTaskFactory owner; + + /// + /// Store the task's initial creationOptions. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly JoinableTaskCreationOptions creationOptions; /// - /// Tracks asynchronous operations and provides the ability to Join those operations to avoid - /// deadlocks while synchronously blocking the Main thread for the operation's completion. + /// The serializable token associated with this particular . /// /// - /// For more complete comments please see the . + /// This will be created when the was created with a parent token + /// or lazily created when this needs to be serialized. /// - [DebuggerDisplay("IsCompleted: {IsCompleted}, Method = {EntryMethodInfo != null ? EntryMethodInfo.Name : null}")] - public partial class JoinableTask : IJoinableTaskDependent - { - /// - /// Stores the top-most JoinableTask that is completing on the current thread, if any. - /// - private static readonly ThreadLocal CompletingTask = new ThreadLocal(); + private SerializableToken? token; - /// - /// The that began the async operation. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private readonly JoinableTaskFactory owner; + /// + /// Other instances of that should be posted + /// to with any main thread bound work. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private ListOfOftenOne nestingFactories; - /// - /// Store the task's initial creationOptions. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private readonly JoinableTaskCreationOptions creationOptions; + /// + /// The to track dependencies between tasks. + /// + private JoinableTaskDependencyGraph.JoinableTaskDependentData dependentData; - /// - /// Other instances of that should be posted - /// to with any main thread bound work. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private ListOfOftenOne nestingFactories; + /// + /// The collections that this job is a member of. + /// + private RarelyRemoveItemSet dependencyParents; - /// - /// The to track dependencies between tasks. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private JoinableTaskDependencyGraph.JoinableTaskDependentData dependentData; + /// + /// The returned by the async delegate that this JoinableTask originally executed, + /// or a if the property was observed before + /// had given us a Task. + /// + /// + /// This is until after returns a (or the property is observed), + /// and retains its value even after this JoinableTask completes. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private object? wrappedTask; - /// - /// The collections that this job is a member of. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private ListOfOftenOne dependencyParents; + /// + /// An event that is signaled when any queue in the dependent has item to process. Lazily constructed. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private AsyncManualResetEvent? queueNeedProcessEvent; - /// - /// The returned by the async delegate that this JoinableTask originally executed, - /// or a if the property was observed before - /// had given us a Task. - /// - /// - /// This is null until after returns a (or the property is observed), - /// and retains its value even after this JoinableTask completes. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private object? wrappedTask; + /// + /// The is triggered by this JoinableTask, this allows a quick access to the event. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private WeakReference? pendingEventSource; - /// - /// An event that is signaled when any queue in the dependent has item to process. Lazily constructed. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private AsyncManualResetEvent? queueNeedProcessEvent; + /// + /// The uplimit of the number pending events. The real number can be less because dependency can be removed, or a pending event can be processed. + /// The number is critical, so it should only be updated in the lock region. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private int pendingEventCount; - /// - /// The is triggered by this JoinableTask, this allows a quick access to the event. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private WeakReference? pendingEventSource; + /// The queue of work items. Lazily constructed. + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private ExecutionQueue? mainThreadQueue; - /// - /// The uplimit of the number pending events. The real number can be less because dependency can be removed, or a pending event can be processed. - /// The number is critical, so it should only be updated in the lock region. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private int pendingEventCount; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private ExecutionQueue? threadPoolQueue; - /// The queue of work items. Lazily constructed. - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private ExecutionQueue? mainThreadQueue; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private volatile JoinableTaskFlags state; - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private ExecutionQueue? threadPoolQueue; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private JoinableTaskSynchronizationContext? mainThreadJobSyncContext; - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private volatile JoinableTaskFlags state; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private JoinableTaskSynchronizationContext? threadPoolJobSyncContext; - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private JoinableTaskSynchronizationContext? mainThreadJobSyncContext; + /// + /// Stores the task's initial delegate so we could show its full name in hang report. + /// This may not *actually* be the real delegate that was invoked for this instance, but + /// it's the meaningful one that should be shown in hang reports. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private Delegate? initialDelegate; - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private JoinableTaskSynchronizationContext? threadPoolJobSyncContext; + /// + /// Backing field for the property. + /// + private WeakReference? weakSelf; + /// + /// Initializes a new instance of the class. + /// + /// The instance that began the async operation. + /// A value indicating whether the launching thread will synchronously block for this job's completion. + /// An optional token that identifies one or more instances, typically in other processes, that serve as 'parents' to this one. + /// The used to customize the task's behavior. + /// The entry method's info for diagnostics. + internal JoinableTask(JoinableTaskFactory owner, bool synchronouslyBlocking, string? parentToken, JoinableTaskCreationOptions creationOptions, Delegate initialDelegate) + { + Requires.NotNull(owner, nameof(owner)); + + this.owner = owner; + if (synchronouslyBlocking) + { + this.state |= JoinableTaskFlags.StartedSynchronously | JoinableTaskFlags.CompletingSynchronously; + } + + if (owner.Context.IsOnMainThread && !this.JoinableTaskContext.IsNoOpContext) + { + this.state |= JoinableTaskFlags.StartedOnMainThread; + if (synchronouslyBlocking) + { + this.state |= JoinableTaskFlags.SynchronouslyBlockingMainThread; + } + } + + this.creationOptions = creationOptions; + this.token = SerializableToken.From(parentToken, this); + this.owner.Context.OnJoinableTaskStarted(this); + this.initialDelegate = initialDelegate; + } + + [Flags] + internal enum JoinableTaskFlags + { /// - /// Stores the task's initial delegate so we could show its full name in hang report. - /// This may not *actually* be the real delegate that was invoked for this instance, but - /// it's the meaningful one that should be shown in hang reports. + /// No other flags defined. /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private Delegate? initialDelegate; + None = 0x0, /// - /// Backing field for the property. + /// This task was originally started as a synchronously executing one. /// - private WeakReference? weakSelf; + StartedSynchronously = 0x1, /// - /// Initializes a new instance of the class. + /// This task was originally started on the main thread. /// - /// The instance that began the async operation. - /// A value indicating whether the launching thread will synchronously block for this job's completion. - /// The used to customize the task's behavior. - /// The entry method's info for diagnostics. - internal JoinableTask(JoinableTaskFactory owner, bool synchronouslyBlocking, JoinableTaskCreationOptions creationOptions, Delegate initialDelegate) - { - Requires.NotNull(owner, nameof(owner)); - - this.owner = owner; - if (synchronouslyBlocking) - { - this.state |= JoinableTaskFlags.StartedSynchronously | JoinableTaskFlags.CompletingSynchronously; - } + StartedOnMainThread = 0x2, - if (owner.Context.IsOnMainThread) - { - this.state |= JoinableTaskFlags.StartedOnMainThread; - if (synchronouslyBlocking) - { - this.state |= JoinableTaskFlags.SynchronouslyBlockingMainThread; - } - } - - this.creationOptions = creationOptions; - this.owner.Context.OnJoinableTaskStarted(this); - this.initialDelegate = initialDelegate; - } + /// + /// This task has had its Complete method called, but may have lingering continuations to execute. + /// + CompleteRequested = 0x4, - [Flags] - internal enum JoinableTaskFlags - { - /// - /// No other flags defined. - /// - None = 0x0, - - /// - /// This task was originally started as a synchronously executing one. - /// - StartedSynchronously = 0x1, - - /// - /// This task was originally started on the main thread. - /// - StartedOnMainThread = 0x2, - - /// - /// This task has had its Complete method called, but may have lingering continuations to execute. - /// - CompleteRequested = 0x4, - - /// - /// This task has completed. - /// - CompleteFinalized = 0x8, - - /// - /// This exact task has been passed to the method. - /// - CompletingSynchronously = 0x10, - - /// - /// This exact task has been passed to the method - /// on the main thread. - /// - SynchronouslyBlockingMainThread = 0x20, - } + /// + /// This task has completed. + /// + CompleteFinalized = 0x8, /// - /// Gets a value indicating whether the async operation represented by this instance has completed, - /// as represented by its property's value. + /// This exact task has been passed to the method. /// - public bool IsCompleted => this.IsCompleteRequested; + CompletingSynchronously = 0x10, /// - /// Gets the asynchronous task that completes when the async operation completes. + /// This exact task has been passed to the method + /// on the main thread. /// - public Task Task + SynchronouslyBlockingMainThread = 0x20, + } + + /// + /// Gets a value indicating whether the async operation represented by this instance has completed, + /// as represented by its property's value. + /// + public bool IsCompleted => this.IsCompleteRequested; + + /// + /// Gets the asynchronous task that completes when the async operation completes. + /// + public Task Task + { + get { - get + if (this.wrappedTask is null) { - if (this.wrappedTask is null) + using (this.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) { - using (this.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) + lock (this.JoinableTaskContext.SyncContextLock) { - lock (this.JoinableTaskContext.SyncContextLock) + if (this.wrappedTask is null) { - if (this.wrappedTask is null) - { - // We'd rather not do this. The field is assigned elsewhere later on if we haven't hit this first. - // But some caller needs a Task that we don't yet have, so we have to spin one up. - this.wrappedTask = this.CreateTaskCompletionSource(); - } + // We'd rather not do this. The field is assigned elsewhere later on if we haven't hit this first. + // But some caller needs a Task that we don't yet have, so we have to spin one up. + this.wrappedTask = this.CreateTaskCompletionSource(); } } } - - // Read 'wrappedTask' once to a local variable. Since this read occurs outside a lock, we need to ensure - // that writes to the field between the 'Task' type check and the call to 'GetTaskFromCompletionSource' - // do not result in passing the wrong object type to the latter (which would result in an - // InvalidCastException). - var wrappedTask = this.wrappedTask; - return wrappedTask as Task ?? this.GetTaskFromCompletionSource(wrappedTask); } + + // Read 'wrappedTask' once to a local variable. Since this read occurs outside a lock, we need to ensure + // that writes to the field between the 'Task' type check and the call to 'GetTaskFromCompletionSource' + // do not result in passing the wrong object type to the latter (which would result in an + // InvalidCastException). + var wrappedTask = this.wrappedTask; + return wrappedTask as Task ?? this.GetTaskFromCompletionSource(wrappedTask); } + } - JoinableTaskContext IJoinableTaskDependent.JoinableTaskContext => this.JoinableTaskContext; + JoinableTaskContext IJoinableTaskDependent.JoinableTaskContext => this.JoinableTaskContext; - bool IJoinableTaskDependent.NeedRefCountChildDependencies => true; + bool IJoinableTaskDependent.NeedRefCountChildDependencies => true; - /// - /// Gets the JoinableTask that is completing (i.e. synchronously blocking) on this thread, nearest to the top of the callstack. - /// - /// - /// This property is intentionally non-public to avoid its abuse by outside callers. - /// - internal static JoinableTask? TaskCompletingOnThisThread - { - get { return CompletingTask.Value; } - } + /// + /// Gets the JoinableTask that is completing (i.e. synchronously blocking) on this thread, nearest to the top of the callstack. + /// + /// + /// This property is intentionally non-public to avoid its abuse by outside callers. + /// + internal static JoinableTask? TaskCompletingOnThisThread + { + get { return CompletingTask.Value; } + } - /// - /// Gets a value indicating whether an awaiter should capture the - /// . - /// - /// - /// As a library, we generally wouldn't capture the - /// when awaiting, except that where our thread is synchronously blocking anyway, it is actually - /// more efficient to capture the so that the continuation - /// will resume on the blocking thread instead of occupying yet another one in order to execute. - /// In fact, when threadpool starvation conditions exist, resuming on the calling thread - /// can avoid significant delays in executing an often trivial continuation. - /// - internal static bool AwaitShouldCaptureSyncContext => SynchronizationContext.Current is JoinableTaskSynchronizationContext; + /// + /// Gets a value indicating whether an awaiter should capture the + /// . + /// + /// + /// As a library, we generally wouldn't capture the + /// when awaiting, except that where our thread is synchronously blocking anyway, it is actually + /// more efficient to capture the so that the continuation + /// will resume on the blocking thread instead of occupying yet another one in order to execute. + /// In fact, when threadpool starvation conditions exist, resuming on the calling thread + /// can avoid significant delays in executing an often trivial continuation. + /// + internal static bool AwaitShouldCaptureSyncContext => SynchronizationContext.Current is JoinableTaskSynchronizationContext; - /// - /// Gets a value indicating whether the async operation and any extra queues tracked by this instance has completed. - /// - internal bool IsFullyCompleted + /// + /// Gets a value indicating whether the async operation and any extra queues tracked by this instance has completed. + /// + internal bool IsFullyCompleted + { + get { - get + using (this.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) { - using (this.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) + lock (this.JoinableTaskContext.SyncContextLock) { - lock (this.JoinableTaskContext.SyncContextLock) + if (!this.IsCompleteRequested) { - if (!this.IsCompleteRequested) - { - return false; - } - - if (this.mainThreadQueue is object && !this.mainThreadQueue.IsCompleted) - { - return false; - } + return false; + } - if (this.threadPoolQueue is object && !this.threadPoolQueue.IsCompleted) - { - return false; - } + if (this.mainThreadQueue is object && !this.mainThreadQueue.IsCompleted) + { + return false; + } - return true; + if (this.threadPoolQueue is object && !this.threadPoolQueue.IsCompleted) + { + return false; } + + return true; } } } + } - /// - /// Gets or sets the set of nesting factories (excluding ) - /// that own JoinableTasks that are nesting this one. - /// - internal ListOfOftenOne NestingFactories - { - get { return this.nestingFactories; } - set { this.nestingFactories = value; } - } + /// + /// Gets or sets the set of nesting factories (excluding ) + /// that own JoinableTasks that are nesting this one. + /// + internal ListOfOftenOne NestingFactories + { + get { return this.nestingFactories; } + set { this.nestingFactories = value; } + } - internal JoinableTaskFactory Factory - { - get { return this.owner; } - } + internal JoinableTaskFactory Factory + { + get { return this.owner; } + } - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - internal SynchronizationContext? ApplicableJobSyncContext + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + internal SynchronizationContext? ApplicableJobSyncContext + { + get { - get + if (this.JoinableTaskContext.IsOnMainThread && !this.JoinableTaskContext.IsNoOpContext) { - if (this.JoinableTaskContext.IsOnMainThread) + if (this.mainThreadJobSyncContext is null) { - if (this.mainThreadJobSyncContext is null) + using (this.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) { - using (this.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) + lock (this.JoinableTaskContext.SyncContextLock) { - lock (this.JoinableTaskContext.SyncContextLock) + if (this.mainThreadJobSyncContext is null) { - if (this.mainThreadJobSyncContext is null) - { - this.mainThreadJobSyncContext = new JoinableTaskSynchronizationContext(this, true); - } + this.mainThreadJobSyncContext = new JoinableTaskSynchronizationContext(this, true); } } } - - return this.mainThreadJobSyncContext; } - else + + return this.mainThreadJobSyncContext; + } + else + { + // This property only changes from true to false, and it reads a volatile field. + // To avoid (measured) lock contention, we skip the lock, risking that we could potentially + // enter the true block a little more than if we took a lock. But returning a synccontext + // for task whose completion was requested is a safe operation, since every sync context we return + // must be operable after that point anyway. + if (this.SynchronouslyBlockingThreadPool) { - // This property only changes from true to false, and it reads a volatile field. - // To avoid (measured) lock contention, we skip the lock, risking that we could potentially - // enter the true block a little more than if we took a lock. But returning a synccontext - // for task whose completion was requested is a safe operation, since every sync context we return - // must be operable after that point anyway. - if (this.SynchronouslyBlockingThreadPool) + if (this.threadPoolJobSyncContext is null) { - if (this.threadPoolJobSyncContext is null) + using (this.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) { - using (this.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) + lock (this.JoinableTaskContext.SyncContextLock) { - lock (this.JoinableTaskContext.SyncContextLock) + if (this.threadPoolJobSyncContext is null) { - if (this.threadPoolJobSyncContext is null) - { - this.threadPoolJobSyncContext = new JoinableTaskSynchronizationContext(this, false); - } + this.threadPoolJobSyncContext = new JoinableTaskSynchronizationContext(this, false); } } } - - return this.threadPoolJobSyncContext; - } - else - { - // If we're not blocking the threadpool, there is no reason to use a thread pool sync context. - return null; } + + return this.threadPoolJobSyncContext; + } + else + { + // If we're not blocking the threadpool, there is no reason to use a thread pool sync context. + return null; } } } + } - /// - /// Gets a weak reference to this object. - /// - internal WeakReference WeakSelf + /// + /// Gets or sets a value indicating whether CoWait will be prohibited + /// during synchronously blocking waits from code actively running within this . + /// + internal int DisableProcessing + { + get => field; + set { - get + field = value; + if (this.mainThreadJobSyncContext is { } syncContext) { - if (this.weakSelf is null) - { - this.weakSelf = new WeakReference(this); - } + syncContext.ConsiderDisableProcessing(); + } + } + } - return this.weakSelf; + /// + /// Gets a weak reference to this object. + /// + internal WeakReference WeakSelf + { + get + { + if (this.weakSelf is null) + { + this.weakSelf = new WeakReference(this); } + + return this.weakSelf; } + } - /// - /// Gets or sets potential unreachable dependent nodes. - /// This is a special collection only used in synchronized task when there are other tasks which are marked to block it through ref-count code. - /// However, it is possible the reference count is retained by loop-dependencies. This collection tracking those items, - /// so the clean-up logic can run when it becomes necessary. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - internal HashSet? PotentialUnreachableDependents { get; set; } + /// + /// Gets or sets potential unreachable dependent nodes. + /// This is a special collection only used in synchronized task when there are other tasks which are marked to block it through ref-count code. + /// However, it is possible the reference count is retained by loop-dependencies. This collection tracking those items, + /// so the clean-up logic can run when it becomes necessary. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + internal HashSet? PotentialUnreachableDependents { get; set; } - /// - /// Gets a value indicating whether PotentialUnreachableDependents is empty. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - internal bool HasPotentialUnreachableDependents => this.PotentialUnreachableDependents is object && this.PotentialUnreachableDependents.Count != 0; + /// + /// Gets a value indicating whether PotentialUnreachableDependents is empty. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + internal bool HasPotentialUnreachableDependents => this.PotentialUnreachableDependents is object && this.PotentialUnreachableDependents.Count != 0; - /// - /// Gets the flags set on this task. - /// - internal JoinableTaskFlags State - { - get { return this.state; } - } + /// + /// Gets the flags set on this task. + /// + internal JoinableTaskFlags State + { + get { return this.state; } + } - /// - /// Gets the task's initial creationOptions. - /// - internal JoinableTaskCreationOptions CreationOptions - { - get { return this.creationOptions; } - } + /// + /// Gets the task's initial creationOptions. + /// + internal JoinableTaskCreationOptions CreationOptions + { + get { return this.creationOptions; } + } - /// - /// Gets the entry method's info so we could show its full name in hang report. - /// - internal MethodInfo? EntryMethodInfo => this.initialDelegate?.GetMethodInfo(); + /// + /// Gets the entry method's info so we could show its full name in hang report. + /// + internal MethodInfo? EntryMethodInfo => this.initialDelegate?.GetMethodInfo(); - /// - /// Gets a value indicating whether this task has a non-empty queue. - /// FOR DIAGNOSTICS COLLECTION ONLY. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - internal bool HasNonEmptyQueue + /// + /// Gets a value indicating whether this task has a non-empty queue. + /// FOR DIAGNOSTICS COLLECTION ONLY. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + internal bool HasNonEmptyQueue + { + get { - get - { - Assumes.True(Monitor.IsEntered(this.JoinableTaskContext.SyncContextLock)); - return (this.mainThreadQueue is object && this.mainThreadQueue.Count > 0) - || (this.threadPoolQueue is object && this.threadPoolQueue.Count > 0); - } + Assumes.True(Monitor.IsEntered(this.JoinableTaskContext.SyncContextLock)); + return (this.mainThreadQueue is object && this.mainThreadQueue.Count > 0) + || (this.threadPoolQueue is object && this.threadPoolQueue.Count > 0); } + } - /// - /// Gets a snapshot of all work queued to the main thread. - /// FOR DIAGNOSTICS COLLECTION ONLY. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - internal IEnumerable MainThreadQueueContents + /// + /// Gets a snapshot of all work queued to the main thread. + /// FOR DIAGNOSTICS COLLECTION ONLY. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + internal IEnumerable MainThreadQueueContents + { + get { - get + Assumes.True(Monitor.IsEntered(this.JoinableTaskContext.SyncContextLock)); + if (this.mainThreadQueue is null) { - Assumes.True(Monitor.IsEntered(this.JoinableTaskContext.SyncContextLock)); - if (this.mainThreadQueue is null) - { - return Enumerable.Empty(); - } - - return this.mainThreadQueue.ToArray(); + return Enumerable.Empty(); } + + return this.mainThreadQueue.ToArray(); } + } - /// - /// Gets a snapshot of all work queued to synchronously blocking threadpool thread. - /// FOR DIAGNOSTICS COLLECTION ONLY. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - internal IEnumerable ThreadPoolQueueContents + /// + /// Gets a snapshot of all work queued to synchronously blocking threadpool thread. + /// FOR DIAGNOSTICS COLLECTION ONLY. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + internal IEnumerable ThreadPoolQueueContents + { + get { - get + Assumes.True(Monitor.IsEntered(this.JoinableTaskContext.SyncContextLock)); + if (this.threadPoolQueue is null) { - Assumes.True(Monitor.IsEntered(this.JoinableTaskContext.SyncContextLock)); - if (this.threadPoolQueue is null) - { - return Enumerable.Empty(); - } - - return this.threadPoolQueue.ToArray(); + return Enumerable.Empty(); } + + return this.threadPoolQueue.ToArray(); } + } - /// - /// Gets the collections this task belongs to. - /// FOR DIAGNOSTICS COLLECTION ONLY. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - internal IEnumerable ContainingCollections + /// + /// Gets the collections this task belongs to. + /// FOR DIAGNOSTICS COLLECTION ONLY. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + internal IEnumerable ContainingCollections + { + get { - get - { - Assumes.True(Monitor.IsEntered(this.JoinableTaskContext.SyncContextLock)); - return this.dependencyParents.OfType().ToList(); - } + Assumes.True(Monitor.IsEntered(this.JoinableTaskContext.SyncContextLock)); + return this.dependencyParents.ToArray().OfType(); } + } - /// - /// Gets or sets a value indicating whether this task has had its Complete() method called.. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - internal bool IsCompleteRequested + /// + /// Gets or sets a value indicating whether this task has had its Complete() method called.. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + internal bool IsCompleteRequested + { + get { - get - { - return (this.state & JoinableTaskFlags.CompleteRequested) != 0; - } + return (this.state & JoinableTaskFlags.CompleteRequested) != 0; + } - set - { - Assumes.True(value); - this.state |= JoinableTaskFlags.CompleteRequested; - } + set + { + Assumes.True(value); + this.state |= JoinableTaskFlags.CompleteRequested; } + } - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private bool SynchronouslyBlockingThreadPool + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private bool SynchronouslyBlockingThreadPool + { + get { - get - { - JoinableTaskFlags state = this.state; - return (state & JoinableTaskFlags.StartedSynchronously) == JoinableTaskFlags.StartedSynchronously - && (state & JoinableTaskFlags.StartedOnMainThread) != JoinableTaskFlags.StartedOnMainThread - && (state & JoinableTaskFlags.CompleteRequested) != JoinableTaskFlags.CompleteRequested; - } + JoinableTaskFlags state = this.state; + return (state & JoinableTaskFlags.StartedSynchronously) == JoinableTaskFlags.StartedSynchronously + && (state & JoinableTaskFlags.StartedOnMainThread) != JoinableTaskFlags.StartedOnMainThread + && (state & JoinableTaskFlags.CompleteRequested) != JoinableTaskFlags.CompleteRequested; } + } - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private bool SynchronouslyBlockingMainThread + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private bool SynchronouslyBlockingMainThread + { + get { - get - { - JoinableTaskFlags state = this.state; - return (state & JoinableTaskFlags.StartedSynchronously) == JoinableTaskFlags.StartedSynchronously - && (state & JoinableTaskFlags.StartedOnMainThread) == JoinableTaskFlags.StartedOnMainThread - && (state & JoinableTaskFlags.CompleteRequested) != JoinableTaskFlags.CompleteRequested; - } + JoinableTaskFlags state = this.state; + return (state & JoinableTaskFlags.StartedSynchronously) == JoinableTaskFlags.StartedSynchronously + && (state & JoinableTaskFlags.StartedOnMainThread) == JoinableTaskFlags.StartedOnMainThread + && (state & JoinableTaskFlags.CompleteRequested) != JoinableTaskFlags.CompleteRequested; } + } - /// - /// Gets JoinableTaskContext for to access locks. - /// - private JoinableTaskContext JoinableTaskContext => this.owner.Context; + /// + /// Gets JoinableTaskContext for to access locks. + /// + private JoinableTaskContext JoinableTaskContext => this.owner.Context; - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private Task QueueNeedProcessEvent + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private Task QueueNeedProcessEvent + { + get { - get + using (this.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) { - using (this.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) + lock (this.JoinableTaskContext.SyncContextLock) { - lock (this.JoinableTaskContext.SyncContextLock) + if (this.queueNeedProcessEvent is null) { - if (this.queueNeedProcessEvent is null) - { - // We pass in allowInliningWaiters: true, - // since we control all waiters and their continuations - // are benign, and it makes it more efficient. - this.queueNeedProcessEvent = new AsyncManualResetEvent(allowInliningAwaiters: true); - } - - return this.queueNeedProcessEvent.WaitAsync(); + // We pass in allowInliningWaiters: true, + // since we control all waiters and their continuations + // are benign, and it makes it more efficient. + this.queueNeedProcessEvent = new AsyncManualResetEvent(allowInliningAwaiters: true); } + + return this.queueNeedProcessEvent.WaitAsync(); } } } + } - /// - /// Synchronously blocks the calling thread until the operation has completed. - /// If the caller is on the Main thread (or is executing within a JoinableTask that has access to the main thread) - /// the caller's access to the Main thread propagates to this JoinableTask so that it may also access the main thread. - /// - /// A cancellation token that will exit this method before the task is completed. - public void Join(CancellationToken cancellationToken = default(CancellationToken)) + /// + /// Synchronously blocks the calling thread until the operation has completed. + /// If the caller is on the Main thread (or is executing within a JoinableTask that has access to the main thread) + /// the caller's access to the Main thread propagates to this JoinableTask so that it may also access the main thread. + /// + /// A cancellation token that will exit this method before the task is completed. + /// Thrown when is canceled. + /// + /// Any exception thrown by the asynchronous operation is propagated out to the caller of this method. + /// + public void Join(CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (this.IsCompleted) { - cancellationToken.ThrowIfCancellationRequested(); + this.Task.GetAwaiter().GetResult(); // rethrow any exceptions + return; + } - if (this.IsCompleted) - { - this.Task.GetAwaiter().GetResult(); // rethrow any exceptions - return; - } + // We don't simply call this.CompleteOnCurrentThread because that doesn't take CancellationToken. + // And it really can't be made to, since it sets state flags indicating the JoinableTask is + // blocking till completion. + // So instead, we new up a new JoinableTask to do the blocking. But we preserve the initial delegate + // so that if a hang occurs it blames the original JoinableTask. + this.owner.Run( + () => this.JoinAsync(cancellationToken), + JoinableTaskCreationOptions.None, + this.initialDelegate); + } - // We don't simply call this.CompleteOnCurrentThread because that doesn't take CancellationToken. - // And it really can't be made to, since it sets state flags indicating the JoinableTask is - // blocking till completion. - // So instead, we new up a new JoinableTask to do the blocking. But we preserve the initial delegate - // so that if a hang occurs it blames the original JoinableTask. - this.owner.Run( - () => this.JoinAsync(cancellationToken), - JoinableTaskCreationOptions.None, - this.initialDelegate); + /// + /// Shares any access to the main thread the caller may have + /// Joins any main thread affinity of the caller with the asynchronous operation to avoid deadlocks + /// in the event that the main thread ultimately synchronously blocks waiting for the operation to complete. + /// + /// + /// A cancellation token that will revert the Join and cause the returned task to complete + /// before the async operation has completed. + /// + /// A task that completes after the asynchronous operation completes and the join is reverted. + /// Thrown when is canceled. + /// + /// Any exception thrown by the asynchronous operation is propagated out to the caller of this method. + /// + public Task JoinAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (!cancellationToken.CanBeCanceled) + { + // A completed or failed JoinableTask will remove itself from parent dependency chains, so we don't repeat it which requires the sync lock. + _ = this.AmbientJobJoinsThis(); + return this.Task; + } + else + { + return JoinSlowAsync(this, cancellationToken); } - /// - /// Shares any access to the main thread the caller may have - /// Joins any main thread affinity of the caller with the asynchronous operation to avoid deadlocks - /// in the event that the main thread ultimately synchronously blocks waiting for the operation to complete. - /// - /// - /// A cancellation token that will revert the Join and cause the returned task to complete - /// before the async operation has completed. - /// - /// A task that completes after the asynchronous operation completes and the join is reverted. - public async Task JoinAsync(CancellationToken cancellationToken = default(CancellationToken)) + static async Task JoinSlowAsync(JoinableTask me, CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); + // No need to dispose of this except in cancellation case. + JoinRelease dependency = me.AmbientJobJoinsThis(); - using (this.AmbientJobJoinsThis()) + try { - await this.Task.WithCancellation(AwaitShouldCaptureSyncContext, cancellationToken).ConfigureAwait(AwaitShouldCaptureSyncContext); + await me.Task.WithCancellation(continueOnCapturedContext: AwaitShouldCaptureSyncContext, cancellationToken).ConfigureAwait(AwaitShouldCaptureSyncContext); + } + catch (OperationCanceledException) + { + dependency.Dispose(); + throw; } } + } - /// - /// Gets an awaiter that is equivalent to calling . - /// - /// A task whose result is the result of the asynchronous operation. - public TaskAwaiter GetAwaiter() - { - return this.JoinAsync().GetAwaiter(); - } + /// + /// Gets an awaiter that is equivalent to calling . + /// + /// A task whose result is the result of the asynchronous operation. + public TaskAwaiter GetAwaiter() + { + return this.JoinAsync().GetAwaiter(); + } - ref JoinableTaskDependencyGraph.JoinableTaskDependentData IJoinableTaskDependent.GetJoinableTaskDependentData() - { - return ref this.dependentData; - } + ref JoinableTaskDependencyGraph.JoinableTaskDependentData IJoinableTaskDependent.GetJoinableTaskDependentData() + { + return ref this.dependentData; + } - void IJoinableTaskDependent.OnAddedToDependency(IJoinableTaskDependent parentNode) - { - Requires.NotNull(parentNode, nameof(parentNode)); - this.dependencyParents.Add(parentNode); - } + void IJoinableTaskDependent.OnAddedToDependency(IJoinableTaskDependent parentNode) + { + Requires.NotNull(parentNode, nameof(parentNode)); + this.dependencyParents.Add(parentNode); + } + + void IJoinableTaskDependent.OnRemovedFromDependency(IJoinableTaskDependent parentNode) + { + Requires.NotNull(parentNode, nameof(parentNode)); + this.dependencyParents.Remove(parentNode); + } + + void IJoinableTaskDependent.OnDependencyAdded(IJoinableTaskDependent joinChild) + { + } + + void IJoinableTaskDependent.OnDependencyRemoved(IJoinableTaskDependent joinChild) + { + } - void IJoinableTaskDependent.OnRemovedFromDependency(IJoinableTaskDependent parentNode) + /// + /// Gets the full token that should be serialized when this owns the context to be shared. + /// + /// The token; or when this task is already completed. + internal string? GetSerializableToken() + { + if (this.token is null && !this.IsCompleteRequested) { - Requires.NotNull(parentNode, nameof(parentNode)); - this.dependencyParents.Remove(parentNode); + using (this.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) + { + lock (this.JoinableTaskContext.SyncContextLock) + { + this.token ??= SerializableToken.New(this); + } + } } - void IJoinableTaskDependent.OnDependencyAdded(IJoinableTaskDependent joinChild) + return this.token?.ToString(); + } + + /// + /// Looks up the that serves as this instance's parent due to the token provided when this was created. + /// + /// A task; or if no parent token was provided at construction time or no match was found (possibly due to the parent having already completed). + internal JoinableTask? GetTokenizedParent() => this.JoinableTaskContext.Lookup(this.token?.ParentToken); + + /// + /// Gets a very likely value whether the main thread is blocked by this . + /// + internal bool MaybeBlockMainThread() + { + if ((this.State & JoinableTask.JoinableTaskFlags.CompleteFinalized) == JoinableTask.JoinableTaskFlags.CompleteFinalized) { + return false; } - void IJoinableTaskDependent.OnDependencyRemoved(IJoinableTaskDependent joinChild) + if ((this.State & JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread) == JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread) { + return true; } - internal void Post(SendOrPostCallback d, object? state, bool mainThreadAffinitized) + return JoinableTaskDependencyGraph.MaybeHasMainThreadSynchronousTaskWaiting(this); + } + + internal void Post(SendOrPostCallback d, object? state, bool mainThreadAffinitized) + { + using (this.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) { - using (this.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) + SingleExecuteProtector? wrapper = null; + List? eventsNeedNotify = null; // initialized if we should pulse it at the end of the method + bool postToFactory = false; + + bool isCompleteRequested; + bool synchronouslyBlockingMainThread; + lock (this.JoinableTaskContext.SyncContextLock) { - SingleExecuteProtector? wrapper = null; - List? eventsNeedNotify = null; // initialized if we should pulse it at the end of the method - bool postToFactory = false; + isCompleteRequested = this.IsCompleteRequested; + synchronouslyBlockingMainThread = this.SynchronouslyBlockingMainThread; + } - bool isCompleteRequested; - bool synchronouslyBlockingMainThread; - lock (this.JoinableTaskContext.SyncContextLock) - { - isCompleteRequested = this.IsCompleteRequested; - synchronouslyBlockingMainThread = this.SynchronouslyBlockingMainThread; - } + if (isCompleteRequested) + { + // This job has already been marked for completion. + // We need to forward the work to the fallback mechanisms. + postToFactory = true; + } + else + { + bool mainThreadQueueUpdated = false; + bool backgroundThreadQueueUpdated = false; + wrapper = SingleExecuteProtector.Create(this, d, state); - if (isCompleteRequested) - { - // This job has already been marked for completion. - // We need to forward the work to the fallback mechanisms. - postToFactory = true; - } - else - { - bool mainThreadQueueUpdated = false; - bool backgroundThreadQueueUpdated = false; - wrapper = SingleExecuteProtector.Create(this, d, state); + wrapper.RaiseTransitioningEvents(mainThreadAffinitized, synchronouslyBlockingMainThread); - if (ThreadingEventSource.Instance.IsEnabled()) + lock (this.JoinableTaskContext.SyncContextLock) + { + if (mainThreadAffinitized) { - ThreadingEventSource.Instance.PostExecutionStart(wrapper.GetHashCode(), mainThreadAffinitized); - } + if (this.mainThreadQueue is null) + { + this.mainThreadQueue = new ExecutionQueue(this); + } - if (mainThreadAffinitized && !synchronouslyBlockingMainThread) - { - wrapper.RaiseTransitioningEvents(); + // Try to post the message here, but we'll also post to the underlying sync context + // so if this fails (because the operation has completed) we'll still get the work + // done eventually. + this.mainThreadQueue.TryEnqueue(wrapper); + mainThreadQueueUpdated = true; } - - lock (this.JoinableTaskContext.SyncContextLock) + else { - if (mainThreadAffinitized) + if (this.SynchronouslyBlockingThreadPool) { - if (this.mainThreadQueue is null) + if (this.threadPoolQueue is null) { - this.mainThreadQueue = new ExecutionQueue(this); + this.threadPoolQueue = new ExecutionQueue(this); } - // Try to post the message here, but we'll also post to the underlying sync context - // so if this fails (because the operation has completed) we'll still get the work - // done eventually. - this.mainThreadQueue.TryEnqueue(wrapper); - mainThreadQueueUpdated = true; - } - else - { - if (this.SynchronouslyBlockingThreadPool) - { - if (this.threadPoolQueue is null) - { - this.threadPoolQueue = new ExecutionQueue(this); - } - - backgroundThreadQueueUpdated = this.threadPoolQueue.TryEnqueue(wrapper); - if (!backgroundThreadQueueUpdated) - { - ThreadPool.QueueUserWorkItem(SingleExecuteProtector.ExecuteOnceWaitCallback, wrapper); - } - } - else + backgroundThreadQueueUpdated = this.threadPoolQueue.TryEnqueue(wrapper); + if (!backgroundThreadQueueUpdated) { ThreadPool.QueueUserWorkItem(SingleExecuteProtector.ExecuteOnceWaitCallback, wrapper); } } + else + { + ThreadPool.QueueUserWorkItem(SingleExecuteProtector.ExecuteOnceWaitCallback, wrapper); + } + } - if (mainThreadQueueUpdated || backgroundThreadQueueUpdated) + if (mainThreadQueueUpdated || backgroundThreadQueueUpdated) + { + IReadOnlyCollection? tasksNeedNotify = JoinableTaskDependencyGraph.GetDependingSynchronousTasks(this, mainThreadQueueUpdated); + if (tasksNeedNotify.Count > 0) { - IReadOnlyCollection? tasksNeedNotify = JoinableTaskDependencyGraph.GetDependingSynchronousTasks(this, mainThreadQueueUpdated); - if (tasksNeedNotify.Count > 0) + eventsNeedNotify = new List(tasksNeedNotify.Count); + foreach (JoinableTask? taskToNotify in tasksNeedNotify) { - eventsNeedNotify = new List(tasksNeedNotify.Count); - foreach (JoinableTask? taskToNotify in tasksNeedNotify) + if (mainThreadQueueUpdated && taskToNotify != this && taskToNotify.pendingEventCount == 0 && taskToNotify.HasPotentialUnreachableDependents) { - if (mainThreadQueueUpdated && taskToNotify != this && taskToNotify.pendingEventCount == 0 && taskToNotify.HasPotentialUnreachableDependents) + // It is not essential to clean up potential unreachable dependent items before triggering the UI thread, + // because dependencies may change, and invalidate this work. However, we try to do this work in the background thread to make it less likely + // doing the expensive work on the UI thread. + if (JoinableTaskDependencyGraph.CleanUpPotentialUnreachableDependentItems(taskToNotify, out HashSet? reachableNodes) && + !reachableNodes.Contains(this)) { - // It is not essential to clean up potential unreachable dependent items before triggering the UI thread, - // because dependencies may change, and invalidate this work. However, we try to do this work in the background thread to make it less likely - // doing the expensive work on the UI thread. - if (JoinableTaskDependencyGraph.CleanUpPotentialUnreachableDependentItems(taskToNotify, out HashSet? reachableNodes) && - !reachableNodes.Contains(this)) - { - continue; - } + continue; } + } - if (taskToNotify.pendingEventSource is null || taskToNotify == this) - { - taskToNotify.pendingEventSource = this.WeakSelf; - } + if (taskToNotify.pendingEventSource is null || taskToNotify == this) + { + taskToNotify.pendingEventSource = this.WeakSelf; + } - taskToNotify.pendingEventCount++; - if (taskToNotify.queueNeedProcessEvent is object) - { - eventsNeedNotify.Add(taskToNotify.queueNeedProcessEvent); - } + taskToNotify.pendingEventCount++; + if (taskToNotify.queueNeedProcessEvent is object) + { + eventsNeedNotify.Add(taskToNotify.queueNeedProcessEvent); } } } } } + } - // Notify tasks which can process the event queue. - if (eventsNeedNotify is object) + // Notify tasks which can process the event queue. + if (eventsNeedNotify is object) + { + foreach (AsyncManualResetEvent? queueEvent in eventsNeedNotify) { - foreach (AsyncManualResetEvent? queueEvent in eventsNeedNotify) - { - queueEvent.PulseAll(); - } + queueEvent.PulseAll(); } + } + + // We deferred this till after we release our lock earlier in this method since we're calling outside code. + if (postToFactory) + { + Assumes.Null(wrapper); // we avoid using a wrapper in this case because this job transferring ownership to the factory. + this.Factory.Post(d, state, mainThreadAffinitized); + } + else if (mainThreadAffinitized) + { + Assumes.NotNull(wrapper); // this should have been initialized in the above logic. + this.owner.PostToUnderlyingSynchronizationContextOrThreadPool(wrapper); - // We deferred this till after we release our lock earlier in this method since we're calling outside code. - if (postToFactory) - { - Assumes.Null(wrapper); // we avoid using a wrapper in this case because this job transferring ownership to the factory. - this.Factory.Post(d, state, mainThreadAffinitized); - } - else if (mainThreadAffinitized) + foreach (JoinableTaskFactory? nestingFactory in this.nestingFactories) { - Assumes.NotNull(wrapper); // this should have been initialized in the above logic. - this.owner.PostToUnderlyingSynchronizationContextOrThreadPool(wrapper); - - foreach (JoinableTaskFactory? nestingFactory in this.nestingFactories) + if (nestingFactory != this.owner) { - if (nestingFactory != this.owner) - { - nestingFactory.PostToUnderlyingSynchronizationContextOrThreadPool(wrapper); - } + nestingFactory.PostToUnderlyingSynchronizationContextOrThreadPool(wrapper); } } } } + } - /// - /// Instantiate a that can track the ultimate result of . - /// - /// The new task completion source. - /// - /// The implementation should be sure to instantiate a that will - /// NOT inline continuations, since we'll be completing this ourselves, potentially while holding a private lock. - /// - internal virtual object CreateTaskCompletionSource() => new TaskCompletionSourceWithoutInlining(allowInliningContinuations: false); + /// + /// Instantiate a that can track the ultimate result of . + /// + /// The new task completion source. + /// + /// The implementation should be sure to instantiate a that will + /// NOT inline continuations, since we'll be completing this ourselves, potentially while holding a private lock. + /// + internal virtual object CreateTaskCompletionSource() => new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - /// - /// Retrieves the from a . - /// - /// The task completion source. - /// The that will complete with this . - internal virtual Task GetTaskFromCompletionSource(object taskCompletionSource) => ((TaskCompletionSourceWithoutInlining)taskCompletionSource).Task; + /// + /// Retrieves the from a . + /// + /// The task completion source. + /// The that will complete with this . + internal virtual Task GetTaskFromCompletionSource(object taskCompletionSource) => ((TaskCompletionSource)taskCompletionSource).Task; - /// - /// Completes a . - /// - /// The task to read a result from. - /// The created earlier with to apply the result to. - internal virtual void CompleteTaskSourceFromWrappedTask(Task wrappedTask, object taskCompletionSource) => wrappedTask.ApplyResultTo((TaskCompletionSourceWithoutInlining)taskCompletionSource); + /// + /// Completes a . + /// + /// The task to read a result from. + /// The created earlier with to apply the result to. + internal virtual void CompleteTaskSourceFromWrappedTask(Task wrappedTask, object taskCompletionSource) => wrappedTask.ApplyResultTo((TaskCompletionSource)taskCompletionSource); - internal void SetWrappedTask(Task wrappedTask) - { - Requires.NotNull(wrappedTask, nameof(wrappedTask)); + internal void SetWrappedTask(Task wrappedTask) + { + Requires.NotNull(wrappedTask, nameof(wrappedTask)); - using (this.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) + using (this.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) + { + lock (this.JoinableTaskContext.SyncContextLock) { - lock (this.JoinableTaskContext.SyncContextLock) + if (this.wrappedTask is null) { - if (this.wrappedTask is null) - { - this.wrappedTask = wrappedTask; - } + this.wrappedTask = wrappedTask; + } - if (wrappedTask.IsCompleted) - { - this.Complete(wrappedTask); - } - else - { - // Arrange for the wrapped task to complete this job when the task completes. - wrappedTask.ContinueWith( - (t, s) => ((JoinableTask)s!).Complete(t), - this, - CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default); - } + if (wrappedTask.IsCompleted) + { + this.Complete(wrappedTask); + } + else + { + // Arrange for the wrapped task to complete this job when the task completes. + wrappedTask.ContinueWith( + (t, s) => ((JoinableTask)s!).Complete(t), + this, + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); } } } + } - /// - /// Fires when the underlying Task is completed. - /// - /// The actual result from . - internal void Complete(Task wrappedTask) - { - Assumes.NotNull(this.wrappedTask); + /// + /// Fires when the underlying Task is completed. + /// + /// The actual result from . + internal void Complete(Task wrappedTask) + { + Assumes.NotNull(this.wrappedTask); - // If we had to synthesize a Task earlier, then wrappedTask is a TaskCompletionSource, - // which we should now complete. - if (!(this.wrappedTask is Task)) - { - this.CompleteTaskSourceFromWrappedTask(wrappedTask, this.wrappedTask); - } + // If we had to synthesize a Task earlier, then wrappedTask is a TaskCompletionSource, + // which we should now complete. + if (!(this.wrappedTask is Task)) + { + this.CompleteTaskSourceFromWrappedTask(wrappedTask, this.wrappedTask); + } - using (this.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) + using (this.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) + { + AsyncManualResetEvent? queueNeedProcessEvent = null; + lock (this.JoinableTaskContext.SyncContextLock) { - AsyncManualResetEvent? queueNeedProcessEvent = null; - lock (this.JoinableTaskContext.SyncContextLock) + if (!this.IsCompleteRequested) { - if (!this.IsCompleteRequested) + // This must be done *before* setting IsCompleteRequested so that the TaskId property will still return the value we need. + ulong? taskId = this.token?.TaskId; + + this.IsCompleteRequested = true; + + if (taskId.HasValue) { - this.IsCompleteRequested = true; + this.JoinableTaskContext.RemoveSerializableIdentifier(taskId.Value); + } - if (this.mainThreadQueue is object) - { - this.mainThreadQueue.Complete(); - } + if (this.mainThreadQueue is object) + { + this.mainThreadQueue.Complete(); + } - if (this.threadPoolQueue is object) - { - this.threadPoolQueue.Complete(); - } + if (this.threadPoolQueue is object) + { + this.threadPoolQueue.Complete(); + } - this.OnQueueCompleted(); + this.OnQueueCompleted(); - // Always arrange to pulse the event since folks waiting - // will likely want to know that the JoinableTask has completed. - queueNeedProcessEvent = this.queueNeedProcessEvent; + // Always arrange to pulse the event since folks waiting + // will likely want to know that the JoinableTask has completed. + queueNeedProcessEvent = this.queueNeedProcessEvent; - JoinableTaskDependencyGraph.OnTaskCompleted(this); - } + JoinableTaskDependencyGraph.OnTaskCompleted(this); } + } - if (queueNeedProcessEvent is object) - { - // We explicitly do this outside our lock. - queueNeedProcessEvent.PulseAll(); - } + if (queueNeedProcessEvent is object) + { + // We explicitly do this outside our lock. + queueNeedProcessEvent.PulseAll(); } } + } - /// Runs a loop to process all queued work items, returning only when the task is completed. - internal void CompleteOnCurrentThread() + /// Runs a loop to process all queued work items, returning only when the task is completed. + internal void CompleteOnCurrentThread() + { + Assumes.NotNull(this.wrappedTask); + + // "Push" this task onto the TLS field's virtual stack so that on hang reports we know which task to 'blame'. + JoinableTask? priorCompletingTask = CompletingTask.Value; + CompletingTask.Value = this; + try { - Assumes.NotNull(this.wrappedTask); + bool onMainThread = false; + JoinableTaskFlags additionalFlags = JoinableTaskFlags.CompletingSynchronously; + if (this.JoinableTaskContext.IsOnMainThread && !this.JoinableTaskContext.IsNoOpContext) + { + additionalFlags |= JoinableTaskFlags.SynchronouslyBlockingMainThread; + onMainThread = true; + } - // "Push" this task onto the TLS field's virtual stack so that on hang reports we know which task to 'blame'. - JoinableTask? priorCompletingTask = CompletingTask.Value; - CompletingTask.Value = this; - try + this.AddStateFlags(additionalFlags); + + if (!this.IsCompleteRequested) { - bool onMainThread = false; - JoinableTaskFlags additionalFlags = JoinableTaskFlags.CompletingSynchronously; - if (this.JoinableTaskContext.IsOnMainThread) + if (ThreadingEventSource.Instance.IsEnabled()) { - additionalFlags |= JoinableTaskFlags.SynchronouslyBlockingMainThread; - onMainThread = true; + ThreadingEventSource.Instance.CompleteOnCurrentThreadStart(this.GetHashCode(), onMainThread); } - this.AddStateFlags(additionalFlags); - - if (!this.IsCompleteRequested) + using (this.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) { - if (ThreadingEventSource.Instance.IsEnabled()) - { - ThreadingEventSource.Instance.CompleteOnCurrentThreadStart(this.GetHashCode(), onMainThread); - } - - using (this.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) + lock (this.JoinableTaskContext.SyncContextLock) { - lock (this.JoinableTaskContext.SyncContextLock) - { - JoinableTaskDependencyGraph.OnSynchronousTaskStartToBlockWaiting(this, out JoinableTask? pendingRequestTask, out this.pendingEventCount); + JoinableTaskDependencyGraph.OnSynchronousTaskStartToBlockWaiting(this, out JoinableTask? pendingRequestTask, out this.pendingEventCount); - // Add the task to the depending tracking list of itself, so it will monitor the event queue. - this.pendingEventSource = pendingRequestTask?.WeakSelf; - } + // Add the task to the depending tracking list of itself, so it will monitor the event queue. + this.pendingEventSource = pendingRequestTask?.WeakSelf; } + } - if (onMainThread) - { - this.JoinableTaskContext.OnSynchronousJoinableTaskToCompleteOnMainThread(this); - } + if (onMainThread) + { + this.JoinableTaskContext.OnSynchronousJoinableTaskToCompleteOnMainThread(this); + } - try + try + { + // Don't use IsCompleted as the condition because that + // includes queues of posted work that don't have to complete for the + // JoinableTask to be ready to return from the JTF.Run method. + HashSet? visited = null; + while (!this.IsCompleteRequested) { - // Don't use IsCompleted as the condition because that - // includes queues of posted work that don't have to complete for the - // JoinableTask to be ready to return from the JTF.Run method. - HashSet? visited = null; - while (!this.IsCompleteRequested) + if (this.TryDequeueSelfOrDependencies(onMainThread, ref visited, out SingleExecuteProtector? work, out Task? tryAgainAfter)) { - if (this.TryDequeueSelfOrDependencies(onMainThread, ref visited, out SingleExecuteProtector? work, out Task? tryAgainAfter)) - { - work.TryExecute(); - } - else if (tryAgainAfter is object) - { - // prevent referencing tasks which may be GCed during the waiting cycle. - visited?.Clear(); - - ThreadingEventSource.Instance.WaitSynchronouslyStart(); - this.owner.WaitSynchronously(tryAgainAfter); - ThreadingEventSource.Instance.WaitSynchronouslyStop(); - Assumes.True(tryAgainAfter.IsCompleted); - } + work.TryExecute(); } - } - finally - { - JoinableTaskDependencyGraph.OnSynchronousTaskEndToBlockWaiting(this); - } + else if (tryAgainAfter is object) + { + // prevent referencing tasks which may be GCed during the waiting cycle. + visited?.Clear(); - if (ThreadingEventSource.Instance.IsEnabled()) - { - ThreadingEventSource.Instance.CompleteOnCurrentThreadStop(this.GetHashCode()); + ThreadingEventSource.Instance.WaitSynchronouslyStart(); + this.owner.WaitSynchronously(tryAgainAfter); + ThreadingEventSource.Instance.WaitSynchronouslyStop(); + Assumes.True(tryAgainAfter.IsCompleted); + } } } - else + finally { - if (onMainThread) - { - this.JoinableTaskContext.OnSynchronousJoinableTaskToCompleteOnMainThread(this); - } + JoinableTaskDependencyGraph.OnSynchronousTaskEndToBlockWaiting(this); } - // Now that we're about to stop blocking a thread, transfer any work - // that was queued but evidently not required to complete this task - // back to the threadpool so it still gets done. - if (this.threadPoolQueue?.Count > 0) + if (ThreadingEventSource.Instance.IsEnabled()) { - while (this.threadPoolQueue.TryDequeue(out SingleExecuteProtector? executor)) - { - ThreadPool.QueueUserWorkItem(SingleExecuteProtector.ExecuteOnceWaitCallback, executor); - } + ThreadingEventSource.Instance.CompleteOnCurrentThreadStop(this.GetHashCode()); } - - Assumes.True(this.Task.IsCompleted); - this.Task.GetAwaiter().GetResult(); // rethrow any exceptions } - finally + else { - CompletingTask.Value = priorCompletingTask; + if (onMainThread) + { + this.JoinableTaskContext.OnSynchronousJoinableTaskToCompleteOnMainThread(this); + } + } + + // Now that we're about to stop blocking a thread, transfer any work + // that was queued but evidently not required to complete this task + // back to the threadpool so it still gets done. + if (this.threadPoolQueue?.Count > 0) + { + while (this.threadPoolQueue.TryDequeue(out SingleExecuteProtector? executor)) + { + ThreadPool.QueueUserWorkItem(SingleExecuteProtector.ExecuteOnceWaitCallback, executor); + } } + + Assumes.True(this.Task.IsCompleted); + this.Task.GetAwaiter().GetResult(); // rethrow any exceptions + } + finally + { + CompletingTask.Value = priorCompletingTask; + } + } + + internal void OnQueueCompleted() + { + if ((this.state & JoinableTaskFlags.CompleteFinalized) == JoinableTaskFlags.CompleteFinalized) + { + return; } - internal void OnQueueCompleted() + if (this.IsFullyCompleted) { - if ((this.state & JoinableTaskFlags.CompleteFinalized) == JoinableTaskFlags.CompleteFinalized) + // Note this code may execute more than once, as multiple queue completion + // notifications come in. + this.JoinableTaskContext.OnJoinableTaskCompleted(this); + + foreach (IJoinableTaskDependent collection in this.dependencyParents.EnumerateAndClear()) { - return; + JoinableTaskDependencyGraph.RemoveDependency(collection, this, forceCleanup: true); } - if (this.IsFullyCompleted) + if (this.mainThreadJobSyncContext is object) { - // Note this code may execute more than once, as multiple queue completion - // notifications come in. - this.JoinableTaskContext.OnJoinableTaskCompleted(this); + this.mainThreadJobSyncContext.OnCompleted(); + } - foreach (IJoinableTaskDependent? collection in this.dependencyParents) - { - JoinableTaskDependencyGraph.RemoveDependency(collection, this, forceCleanup: true); - } + if (this.threadPoolJobSyncContext is object) + { + this.threadPoolJobSyncContext.OnCompleted(); + } - if (this.mainThreadJobSyncContext is object) - { - this.mainThreadJobSyncContext.OnCompleted(); - } + this.nestingFactories = default(ListOfOftenOne); + this.initialDelegate = null; + this.state |= JoinableTaskFlags.CompleteFinalized; + } + } - if (this.threadPoolJobSyncContext is object) - { - this.threadPoolJobSyncContext.OnCompleted(); - } + /// + /// Get the number of pending messages to be process for the synchronous task. + /// + /// The synchronous task. + /// The number of events need be processed by the synchronous task in the current JoinableTask. + internal int GetPendingEventCountForSynchronousTask(JoinableTask synchronousTask) + { + Requires.NotNull(synchronousTask, nameof(synchronousTask)); + ExecutionQueue? queue = ((synchronousTask.state & JoinableTaskFlags.SynchronouslyBlockingMainThread) == JoinableTaskFlags.SynchronouslyBlockingMainThread) + ? this.mainThreadQueue + : this.threadPoolQueue; + return queue is object ? queue.Count : 0; + } - this.nestingFactories = default(ListOfOftenOne); - this.initialDelegate = null; - this.state |= JoinableTaskFlags.CompleteFinalized; - } - } + /// + /// This is a helper method to parepare notifing the sychronous task for pending events. + /// It must be called inside JTF lock, and returns a collection of event to trigger later. (Those events must be triggered out of the JTF lock.) + /// + internal AsyncManualResetEvent? RegisterPendingEventsForSynchrousTask(JoinableTask taskHasPendingMessages, int newPendingMessagesCount) + { + Requires.NotNull(taskHasPendingMessages, nameof(taskHasPendingMessages)); + Requires.Range(newPendingMessagesCount > 0, nameof(newPendingMessagesCount)); + Assumes.True(Monitor.IsEntered(this.JoinableTaskContext.SyncContextLock)); + Assumes.True((this.state & JoinableTaskFlags.CompletingSynchronously) == JoinableTaskFlags.CompletingSynchronously); - /// - /// Get the number of pending messages to be process for the synchronous task. - /// - /// The synchronous task. - /// The number of events need be processed by the synchronous task in the current JoinableTask. - internal int GetPendingEventCountForSynchronousTask(JoinableTask synchronousTask) + if (this.pendingEventSource is null || taskHasPendingMessages == this) { - Requires.NotNull(synchronousTask, nameof(synchronousTask)); - ExecutionQueue? queue = ((synchronousTask.state & JoinableTaskFlags.SynchronouslyBlockingMainThread) == JoinableTaskFlags.SynchronouslyBlockingMainThread) - ? this.mainThreadQueue - : this.threadPoolQueue; - return queue is object ? queue.Count : 0; + this.pendingEventSource = taskHasPendingMessages.WeakSelf; } - /// - /// This is a helper method to parepare notifing the sychronous task for pending events. - /// It must be called inside JTF lock, and returns a collection of event to trigger later. (Those events must be triggered out of the JTF lock.) - /// - internal AsyncManualResetEvent? RegisterPendingEventsForSynchrousTask(JoinableTask taskHasPendingMessages, int newPendingMessagesCount) - { - Requires.NotNull(taskHasPendingMessages, nameof(taskHasPendingMessages)); - Requires.Range(newPendingMessagesCount > 0, nameof(newPendingMessagesCount)); - Assumes.True(Monitor.IsEntered(this.JoinableTaskContext.SyncContextLock)); - Assumes.True((this.state & JoinableTaskFlags.CompletingSynchronously) == JoinableTaskFlags.CompletingSynchronously); + this.pendingEventCount += newPendingMessagesCount; + return this.queueNeedProcessEvent; + } - if (this.pendingEventSource is null || taskHasPendingMessages == this) + private protected JoinRelease AmbientJobJoinsThis() + { + if (!this.IsCompleted) + { + JoinableTask? ambientJob = this.JoinableTaskContext.AmbientTask; + if (ambientJob is object && ambientJob != this) { - this.pendingEventSource = taskHasPendingMessages.WeakSelf; + return JoinableTaskDependencyGraph.AddDependency(ambientJob, this); } - - this.pendingEventCount += newPendingMessagesCount; - return this.queueNeedProcessEvent; } - private static bool TryDequeueSelfOrDependencies(IJoinableTaskDependent currentNode, bool onMainThread, HashSet visited, [NotNullWhen(true)] out SingleExecuteProtector? work) - { - Requires.NotNull(currentNode, nameof(currentNode)); - Requires.NotNull(visited, nameof(visited)); - Report.IfNot(Monitor.IsEntered(currentNode.JoinableTaskContext.SyncContextLock)); + return default(JoinRelease); + } - // We only need to find the first work item. - work = null; - if (visited.Add(currentNode)) + private static bool TryDequeueSelfOrDependencies(IJoinableTaskDependent currentNode, bool onMainThread, HashSet visited, [NotNullWhen(true)] out SingleExecuteProtector? work) + { + Requires.NotNull(currentNode, nameof(currentNode)); + Requires.NotNull(visited, nameof(visited)); + Report.IfNot(Monitor.IsEntered(currentNode.JoinableTaskContext.SyncContextLock)); + + // We only need to find the first work item. + work = null; + if (visited.Add(currentNode)) + { + JoinableTask? joinableTask = currentNode as JoinableTask; + if (joinableTask is object) { - JoinableTask? joinableTask = currentNode as JoinableTask; - if (joinableTask is object) + ExecutionQueue? queue = onMainThread ? joinableTask.mainThreadQueue : joinableTask.threadPoolQueue; + if (queue is object && !queue.IsCompleted) { - ExecutionQueue? queue = onMainThread ? joinableTask.mainThreadQueue : joinableTask.threadPoolQueue; - if (queue is object && !queue.IsCompleted) - { - queue.TryDequeue(out work); - } + queue.TryDequeue(out work); } + } - if (work is null) + if (work is null) + { + if (joinableTask?.IsCompleteRequested != true) { - if (joinableTask?.IsCompleteRequested != true) + foreach (IJoinableTaskDependent? item in JoinableTaskDependencyGraph.GetDirectDependentNodes(currentNode)) { - foreach (IJoinableTaskDependent? item in JoinableTaskDependencyGraph.GetDirectDependentNodes(currentNode)) + if (TryDequeueSelfOrDependencies(item, onMainThread, visited, out work)) { - if (TryDequeueSelfOrDependencies(item, onMainThread, visited, out work)) - { - break; - } + break; } } } } - - return work is object; } - private bool TryDequeueSelfOrDependencies(bool onMainThread, ref HashSet? visited, [NotNullWhen(true)] out SingleExecuteProtector? work, out Task? tryAgainAfter) + return work is object; + } + + private bool TryDequeueSelfOrDependencies(bool onMainThread, ref HashSet? visited, [NotNullWhen(true)] out SingleExecuteProtector? work, out Task? tryAgainAfter) + { + using (this.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) { - using (this.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) + lock (this.JoinableTaskContext.SyncContextLock) { - lock (this.JoinableTaskContext.SyncContextLock) + if (this.IsFullyCompleted) { - if (this.IsFullyCompleted) - { - work = null; - tryAgainAfter = null; - return false; - } + work = null; + tryAgainAfter = null; + return false; + } - if (this.pendingEventCount > 0) - { - this.pendingEventCount--; + if (this.pendingEventCount > 0) + { + this.pendingEventCount--; - if (this.pendingEventSource is object) + if (this.pendingEventSource is object) + { + if (this.pendingEventSource.TryGetTarget(out JoinableTask? pendingSource) && + (pendingSource == this || + (!this.HasPotentialUnreachableDependents && JoinableTaskDependencyGraph.IsDependingSynchronousTask(pendingSource, this)))) { - if (this.pendingEventSource.TryGetTarget(out JoinableTask? pendingSource) && - (pendingSource == this || - (!this.HasPotentialUnreachableDependents && JoinableTaskDependencyGraph.IsDependingSynchronousTask(pendingSource, this)))) + ExecutionQueue? queue = onMainThread ? pendingSource.mainThreadQueue : pendingSource.threadPoolQueue; + if (queue is object && !queue.IsCompleted && queue.TryDequeue(out work)) { - ExecutionQueue? queue = onMainThread ? pendingSource.mainThreadQueue : pendingSource.threadPoolQueue; - if (queue is object && !queue.IsCompleted && queue.TryDequeue(out work)) + if (queue.Count == 0) { - if (queue.Count == 0) - { - this.pendingEventSource = null; - } - - tryAgainAfter = null; - return true; + this.pendingEventSource = null; } - } - this.pendingEventSource = null; + tryAgainAfter = null; + return true; + } } - if (visited is null) - { - visited = new HashSet(); - } - else - { - visited.Clear(); - } + this.pendingEventSource = null; + } - bool foundWork = TryDequeueSelfOrDependencies(this, onMainThread, visited, out work); + if (visited is null) + { + visited = new HashSet(); + } + else + { + visited.Clear(); + } - HashSet? visitedNodes = visited; - if (this.HasPotentialUnreachableDependents) - { - // We walked the dependencies tree and use this information to update the PotentialUnreachableDependents list. - this.PotentialUnreachableDependents!.RemoveWhere(n => visitedNodes.Contains(n)); + bool foundWork = TryDequeueSelfOrDependencies(this, onMainThread, visited, out work); - if (!foundWork && this.PotentialUnreachableDependents.Count > 0) - { - JoinableTaskDependencyGraph.RemoveUnreachableDependentItems(this, this.PotentialUnreachableDependents, visitedNodes); - this.PotentialUnreachableDependents.Clear(); - } - } + HashSet? visitedNodes = visited; + if (this.HasPotentialUnreachableDependents) + { + // We walked the dependencies tree and use this information to update the PotentialUnreachableDependents list. + this.PotentialUnreachableDependents!.RemoveWhere(n => visitedNodes.Contains(n)); - if (foundWork) + if (!foundWork && this.PotentialUnreachableDependents.Count > 0) { - Assumes.NotNull(work); - - tryAgainAfter = null; - return true; + JoinableTaskDependencyGraph.RemoveUnreachableDependentItems(this, this.PotentialUnreachableDependents, visitedNodes); + this.PotentialUnreachableDependents.Clear(); } } - this.pendingEventCount = 0; + if (foundWork) + { + Assumes.NotNull(work); + + tryAgainAfter = null; + return true; + } + } + + this.pendingEventCount = 0; - work = null; - tryAgainAfter = this.IsCompleteRequested ? null : this.QueueNeedProcessEvent; - return false; + work = null; + tryAgainAfter = this.IsCompleteRequested ? null : this.QueueNeedProcessEvent; + return false; + } + } + } + + /// + /// Adds the specified flags to the field. + /// + private void AddStateFlags(JoinableTaskFlags flags) + { + // Try to avoid taking a lock if the flags are already set appropriately. + if ((this.state & flags) != flags) + { + using (this.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) + { + lock (this.JoinableTaskContext.SyncContextLock) + { + this.state |= flags; } } } + } + + private class SerializableToken + { + private readonly JoinableTask owner; + private ulong? taskId; + private string? fullToken; + + private SerializableToken(JoinableTask owner) + { + this.owner = owner; + } + + /// + /// Gets the original token provided by the remote parent task. + /// + internal string? ParentToken { get; init; } /// - /// Adds the specified flags to the field. + /// Gets the unique ID that identifies the owning in the dictionary. /// - private void AddStateFlags(JoinableTaskFlags flags) + internal ulong? TaskId { - // Try to avoid taking a lock if the flags are already set appropriately. - if ((this.state & flags) != flags) + get { - using (this.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) + if (this.taskId is null && !this.owner.IsCompleteRequested) { - lock (this.JoinableTaskContext.SyncContextLock) + using (this.owner.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) { - this.state |= flags; + lock (this.owner.JoinableTaskContext.SyncContextLock) + { + if (this.taskId is null && !this.owner.IsCompleteRequested) + { + this.taskId = this.owner.JoinableTaskContext.AssignUniqueIdentifier(this.owner); + } + } } } + + return this.owner.IsCompleteRequested ? null : this.taskId; } } - private JoinRelease AmbientJobJoinsThis() + /// + /// Serializes this token as a string. + /// + /// A string that identifies the owner and retains information about its parents, if any. May be if the owning task has already completed. + public override string? ToString() { - if (!this.IsCompleted) + if (this.fullToken is null && this.TaskId is ulong taskId) { - JoinableTask? ambientJob = this.JoinableTaskContext.AmbientTask; - if (ambientJob is object && ambientJob != this) - { - return JoinableTaskDependencyGraph.AddDependency(ambientJob, this); - } + this.fullToken = this.owner.JoinableTaskContext.ConstructFullToken(taskId, this.ParentToken); } - return default(JoinRelease); + return this.owner.IsCompleteRequested ? null : this.fullToken; } + + /// + /// Creates a when a parent token is provided. + /// + /// The parent token, if any. + /// The owning task. + /// The token, if any is required. + [return: NotNullIfNotNull(nameof(parentToken))] + internal static SerializableToken? From(string? parentToken, JoinableTask owner) => parentToken is null ? null : new SerializableToken(owner) { ParentToken = parentToken }; + + /// + /// Creates a for a given if it has not yet completed. + /// + /// The owning task. + /// A token, if the task has not yet completed; otherwise . + internal static SerializableToken? New(JoinableTask owner) => owner.IsCompleteRequested ? null : new SerializableToken(owner); } } diff --git a/src/Microsoft.VisualStudio.Threading/JoinableTaskCollection.cs b/src/Microsoft.VisualStudio.Threading/JoinableTaskCollection.cs index 20dd6465f..e929d97b6 100644 --- a/src/Microsoft.VisualStudio.Threading/JoinableTaskCollection.cs +++ b/src/Microsoft.VisualStudio.Threading/JoinableTaskCollection.cs @@ -1,292 +1,296 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// A collection of incomplete objects. +/// +/// +/// Any completed is automatically removed from the collection. +/// +[DebuggerDisplay("JoinableTaskCollection: {displayName ?? \"(anonymous)\"}")] +public class JoinableTaskCollection : IJoinableTaskDependent, IEnumerable { - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Threading; - using System.Threading.Tasks; + /// + /// A value indicating whether joinable tasks are only removed when completed or removed as many times as they were added. + /// + private readonly bool refCountAddedJobs; + + /// + /// A human-readable name that may appear in hang reports. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string? displayName; + + /// + /// The to track dependencies between tasks. + /// + private JoinableTaskDependencyGraph.JoinableTaskDependentData dependentData; + + /// + /// An event that is set when the collection is empty (lazily initialized). + /// + private AsyncManualResetEvent? emptyEvent; + + /// + /// Initializes a new instance of the class. + /// + /// The instance to which this collection applies. + /// + /// if JoinableTask instances added to the collection multiple times should remain in the collection until they are + /// either removed the same number of times or until they are completed; + /// causes the first Remove call for a JoinableTask to remove it from this collection regardless + /// how many times it had been added. + public JoinableTaskCollection(JoinableTaskContext context, bool refCountAddedJobs = false) + { + Requires.NotNull(context, nameof(context)); + this.Context = context; + this.refCountAddedJobs = refCountAddedJobs; + } /// - /// A collection of incomplete objects. + /// Gets the to which this collection belongs. + /// + public JoinableTaskContext Context { get; } + + /// + /// Gets or sets a human-readable name that may appear in hang reports. /// /// - /// Any completed is automatically removed from the collection. + /// This property should *not* be set to a value that may disclose + /// personally identifiable information or other confidential data + /// since this value may be included in hang reports sent to a third party. /// - [DebuggerDisplay("JoinableTaskCollection: {displayName ?? \"(anonymous)\"}")] - public class JoinableTaskCollection : IJoinableTaskDependent, IEnumerable + public string? DisplayName { - /// - /// A value indicating whether joinable tasks are only removed when completed or removed as many times as they were added. - /// - private readonly bool refCountAddedJobs; + get { return this.displayName; } + set { this.displayName = value; } + } - /// - /// A human-readable name that may appear in hang reports. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private string? displayName; + /// + /// Gets JoinableTaskContext for to access locks. + /// + JoinableTaskContext IJoinableTaskDependent.JoinableTaskContext => this.Context; - /// - /// The to track dependencies between tasks. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private JoinableTaskDependencyGraph.JoinableTaskDependentData dependentData; + /// + /// Gets a value indicating whether we need count reference for child dependent nodes. + /// + bool IJoinableTaskDependent.NeedRefCountChildDependencies => this.refCountAddedJobs; - /// - /// An event that is set when the collection is empty (lazily initialized). - /// - private AsyncManualResetEvent? emptyEvent; + ref JoinableTaskDependencyGraph.JoinableTaskDependentData IJoinableTaskDependent.GetJoinableTaskDependentData() => ref this.dependentData; - /// - /// Initializes a new instance of the class. - /// - /// The instance to which this collection applies. - /// - /// true if JoinableTask instances added to the collection multiple times should remain in the collection until they are - /// either removed the same number of times or until they are completed; - /// false causes the first Remove call for a JoinableTask to remove it from this collection regardless - /// how many times it had been added. - public JoinableTaskCollection(JoinableTaskContext context, bool refCountAddedJobs = false) + /// + /// Adds the specified to this collection. + /// + /// The to add to the collection. + /// + /// As the collection only stores *incomplete* instances, + /// if the is already completed, it will not be added to the collection and this method will simply return. + /// Any instances added to the collection will be automatically removed upon completion. + /// + public void Add(JoinableTask joinableTask) + { + Requires.NotNull(joinableTask, nameof(joinableTask)); + if (joinableTask.Factory.Context != this.Context) { - Requires.NotNull(context, nameof(context)); - this.Context = context; - this.refCountAddedJobs = refCountAddedJobs; + Requires.Argument(false, "joinableTask", Strings.JoinableTaskContextAndCollectionMismatch); } - /// - /// Gets the to which this collection belongs. - /// - public JoinableTaskContext Context { get; } + JoinableTaskDependencyGraph.AddDependency(this, joinableTask); + } - /// - /// Gets or sets a human-readable name that may appear in hang reports. - /// - /// - /// This property should *not* be set to a value that may disclose - /// personally identifiable information or other confidential data - /// since this value may be included in hang reports sent to a third party. - /// - public string? DisplayName + /// + /// Removes the specified from this collection, + /// or decrements the ref count if this collection tracks that. + /// + /// The to remove. + /// + /// Completed instances are automatically removed from the collection. + /// Calling this method to remove them is not necessary. + /// + public void Remove(JoinableTask joinableTask) + { + Requires.NotNull(joinableTask, nameof(joinableTask)); + JoinableTaskDependencyGraph.RemoveDependency(this, joinableTask); + } + + /// + /// Shares access to the main thread that the caller's JoinableTask may have (if any) with all + /// JoinableTask instances in this collection until the returned value is disposed. + /// + /// A value to dispose of to revert the join. + /// + /// Calling this method when the caller is not executing within a JoinableTask safely no-ops. + /// + public JoinRelease Join() + { + JoinableTask? ambientJob = this.Context.AmbientTask; + if (ambientJob is null) { - get { return this.displayName; } - set { this.displayName = value; } + // The caller isn't running in the context of a joinable task, so there is nothing to join with this collection. + return default(JoinRelease); } - /// - /// Gets JoinableTaskContext for to access locks. - /// - JoinableTaskContext IJoinableTaskDependent.JoinableTaskContext => this.Context; + return JoinableTaskDependencyGraph.AddDependency(ambientJob, this); + } - /// - /// Gets a value indicating whether we need count reference for child dependent nodes. - /// - bool IJoinableTaskDependent.NeedRefCountChildDependencies => this.refCountAddedJobs; + /// + /// Joins the caller's context to this collection till the collection is empty. + /// + /// A task that completes when this collection is empty. + /// + /// Any exceptions thrown by the tasks in this collection are not propagated to the returned task. + /// + public Task JoinTillEmptyAsync() => this.JoinTillEmptyAsync(CancellationToken.None); - ref JoinableTaskDependencyGraph.JoinableTaskDependentData IJoinableTaskDependent.GetJoinableTaskDependentData() => ref this.dependentData; + /// + /// Joins the caller's context to this collection till the collection is empty. + /// + /// A cancellation token. + /// A task that completes when this collection is empty, or is canceled when is canceled. + /// + /// Any exceptions thrown by the tasks in this collection are not propagated to the returned task. + /// + public async Task JoinTillEmptyAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); - /// - /// Adds the specified to this collection. - /// - /// The to add to the collection. - /// - /// As the collection only stores *incomplete* instances, - /// if the is already completed, it will not be added to the collection and this method will simply return. - /// Any instances added to the collection will be automatically removed upon completion. - /// - public void Add(JoinableTask joinableTask) + if (this.emptyEvent is null) { - Requires.NotNull(joinableTask, nameof(joinableTask)); - if (joinableTask.Factory.Context != this.Context) + // We need a read lock to protect against the emptiness of this collection changing + // while we're setting the initial set state of the new event. + using (this.Context.NoMessagePumpSynchronizationContext.Apply()) { - Requires.Argument(false, "joinableTask", Strings.JoinableTaskContextAndCollectionMismatch); + lock (this.Context.SyncContextLock) + { + if (this.emptyEvent is null) + { + this.emptyEvent = new AsyncManualResetEvent(JoinableTaskDependencyGraph.HasNoChildDependentNode(this)); + } + } } - - JoinableTaskDependencyGraph.AddDependency(this, joinableTask); } - /// - /// Removes the specified from this collection, - /// or decrements the ref count if this collection tracks that. - /// - /// The to remove. - /// - /// Completed instances are automatically removed from the collection. - /// Calling this method to remove them is not necessary. - /// - public void Remove(JoinableTask joinableTask) + using (this.Join()) { - Requires.NotNull(joinableTask, nameof(joinableTask)); - JoinableTaskDependencyGraph.RemoveDependency(this, joinableTask); + await this.emptyEvent.WaitAsync(cancellationToken).ConfigureAwaitRunInline(); } + } - /// - /// Shares access to the main thread that the caller's JoinableTask may have (if any) with all - /// JoinableTask instances in this collection until the returned value is disposed. - /// - /// A value to dispose of to revert the join. - /// - /// Calling this method when the caller is not executing within a JoinableTask safely no-ops. - /// - public JoinRelease Join() + /// + /// Checks whether the specified joinable task is a member of this collection. + /// + public bool Contains(JoinableTask joinableTask) + { + Requires.NotNull(joinableTask, nameof(joinableTask)); + + using (this.Context.NoMessagePumpSynchronizationContext.Apply()) { - JoinableTask? ambientJob = this.Context.AmbientTask; - if (ambientJob is null) + lock (this.Context.SyncContextLock) { - // The caller isn't running in the context of a joinable task, so there is nothing to join with this collection. - return default(JoinRelease); + return JoinableTaskDependencyGraph.HasDirectDependency(this, joinableTask); } - - return JoinableTaskDependencyGraph.AddDependency(ambientJob, this); } + } - /// - /// Joins the caller's context to this collection till the collection is empty. - /// - /// A task that completes when this collection is empty. - public Task JoinTillEmptyAsync() => this.JoinTillEmptyAsync(CancellationToken.None); - - /// - /// Joins the caller's context to this collection till the collection is empty. - /// - /// A cancellation token. - /// A task that completes when this collection is empty, or is canceled when is canceled. - public async Task JoinTillEmptyAsync(CancellationToken cancellationToken) + /// + /// Enumerates the tasks in this collection. + /// + public IEnumerator GetEnumerator() + { + using (this.Context.NoMessagePumpSynchronizationContext.Apply()) { - cancellationToken.ThrowIfCancellationRequested(); - - if (this.emptyEvent is null) + var joinables = new List(); + lock (this.Context.SyncContextLock) { - // We need a read lock to protect against the emptiness of this collection changing - // while we're setting the initial set state of the new event. - using (this.Context.NoMessagePumpSynchronizationContext.Apply()) + foreach (IJoinableTaskDependent? item in JoinableTaskDependencyGraph.GetDirectDependentNodes(this)) { - lock (this.Context.SyncContextLock) + if (item is JoinableTask joinableTask) { - if (this.emptyEvent is null) - { - this.emptyEvent = new AsyncManualResetEvent(JoinableTaskDependencyGraph.HasNoChildDependentNode(this)); - } + joinables.Add(joinableTask); } } } - using (this.Join()) - { - await this.emptyEvent.WaitAsync(cancellationToken).ConfigureAwaitRunInline(); - } + return joinables.GetEnumerator(); } + } - /// - /// Checks whether the specified joinable task is a member of this collection. - /// - public bool Contains(JoinableTask joinableTask) - { - Requires.NotNull(joinableTask, nameof(joinableTask)); - - using (this.Context.NoMessagePumpSynchronizationContext.Apply()) - { - lock (this.Context.SyncContextLock) - { - return JoinableTaskDependencyGraph.HasDirectDependency(this, joinableTask); - } - } - } + /// + /// Enumerates the tasks in this collection. + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } - /// - /// Enumerates the tasks in this collection. - /// - public IEnumerator GetEnumerator() - { - using (this.Context.NoMessagePumpSynchronizationContext.Apply()) - { - var joinables = new List(); - lock (this.Context.SyncContextLock) - { - foreach (IJoinableTaskDependent? item in JoinableTaskDependencyGraph.GetDirectDependentNodes(this)) - { - if (item is JoinableTask joinableTask) - { - joinables.Add(joinableTask); - } - } - } + void IJoinableTaskDependent.OnAddedToDependency(IJoinableTaskDependent parent) + { + } - return joinables.GetEnumerator(); - } - } + void IJoinableTaskDependent.OnRemovedFromDependency(IJoinableTaskDependent parentNode) + { + } - /// - /// Enumerates the tasks in this collection. - /// - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + void IJoinableTaskDependent.OnDependencyAdded(IJoinableTaskDependent joinChild) + { + if (this.emptyEvent is object && joinChild is JoinableTask) { - return this.GetEnumerator(); + this.emptyEvent.Reset(); } + } - void IJoinableTaskDependent.OnAddedToDependency(IJoinableTaskDependent parent) + void IJoinableTaskDependent.OnDependencyRemoved(IJoinableTaskDependent joinChild) + { + if (this.emptyEvent is object && JoinableTaskDependencyGraph.HasNoChildDependentNode(this)) { + this.emptyEvent.Set(); } + } - void IJoinableTaskDependent.OnRemovedFromDependency(IJoinableTaskDependent parentNode) - { - } + /// + /// A value whose disposal cancels a operation. + /// + public struct JoinRelease : IDisposable + { + private IJoinableTaskDependent? parentDependencyNode; + private IJoinableTaskDependent? childDependencyNode; - void IJoinableTaskDependent.OnDependencyAdded(IJoinableTaskDependent joinChild) + /// + /// Initializes a new instance of the struct. + /// + /// The Main thread controlling SingleThreadSynchronizationContext to use to accelerate execution of Main thread bound work. + /// The instance that created this value. + internal JoinRelease(IJoinableTaskDependent parentDependencyNode, IJoinableTaskDependent childDependencyNode) { - if (this.emptyEvent is object && joinChild is JoinableTask) - { - this.emptyEvent.Reset(); - } - } + Requires.NotNull(parentDependencyNode, nameof(parentDependencyNode)); + Requires.NotNull(childDependencyNode, nameof(childDependencyNode)); - void IJoinableTaskDependent.OnDependencyRemoved(IJoinableTaskDependent joinChild) - { - if (this.emptyEvent is object && JoinableTaskDependencyGraph.HasNoChildDependentNode(this)) - { - this.emptyEvent.Set(); - } + this.parentDependencyNode = parentDependencyNode; + this.childDependencyNode = childDependencyNode; } /// - /// A value whose disposal cancels a operation. + /// Cancels the operation. /// - public struct JoinRelease : IDisposable + public void Dispose() { - private IJoinableTaskDependent? parentDependencyNode; - private IJoinableTaskDependent? childDependencyNode; - - /// - /// Initializes a new instance of the struct. - /// - /// The Main thread controlling SingleThreadSynchronizationContext to use to accelerate execution of Main thread bound work. - /// The instance that created this value. - internal JoinRelease(IJoinableTaskDependent parentDependencyNode, IJoinableTaskDependent childDependencyNode) + if (this.parentDependencyNode is object) { - Requires.NotNull(parentDependencyNode, nameof(parentDependencyNode)); - Requires.NotNull(childDependencyNode, nameof(childDependencyNode)); + RoslynDebug.Assert(this.childDependencyNode is object, $"{nameof(this.childDependencyNode)} can only be null when {nameof(this.parentDependencyNode)} is null."); - this.parentDependencyNode = parentDependencyNode; - this.childDependencyNode = childDependencyNode; + JoinableTaskDependencyGraph.RemoveDependency(this.parentDependencyNode, this.childDependencyNode); + this.parentDependencyNode = null; } - /// - /// Cancels the operation. - /// - public void Dispose() - { - if (this.parentDependencyNode is object) - { - RoslynDebug.Assert(this.childDependencyNode is object, $"{nameof(this.childDependencyNode)} can only be null when {nameof(this.parentDependencyNode)} is null."); - - JoinableTaskDependencyGraph.RemoveDependency(this.parentDependencyNode, this.childDependencyNode); - this.parentDependencyNode = null; - } - - this.childDependencyNode = null; - } + this.childDependencyNode = null; } } } diff --git a/src/Microsoft.VisualStudio.Threading/JoinableTaskContext+HangReportContributor.cs b/src/Microsoft.VisualStudio.Threading/JoinableTaskContext+HangReportContributor.cs index d86ff1965..caad35d6c 100644 --- a/src/Microsoft.VisualStudio.Threading/JoinableTaskContext+HangReportContributor.cs +++ b/src/Microsoft.VisualStudio.Threading/JoinableTaskContext+HangReportContributor.cs @@ -1,210 +1,213 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Xml.Linq; + +namespace Microsoft.VisualStudio.Threading; + +public partial class JoinableTaskContext : IHangReportContributor { - using System; - using System.Collections.Generic; - using System.Globalization; - using System.Linq; - using System.Text; - using System.Threading.Tasks; - using System.Xml.Linq; - - public partial class JoinableTaskContext : IHangReportContributor + /// + /// Contributes data for a hang report. + /// + /// The hang report contribution. + [RequiresUnreferencedCode(Reasons.DiagnosticAnalysisOnly)] + HangReportContribution IHangReportContributor.GetHangReport() { - /// - /// Contributes data for a hang report. - /// - /// The hang report contribution. - HangReportContribution IHangReportContributor.GetHangReport() - { - return this.GetHangReport(); - } + return this.GetHangReport(); + } - /// - /// Contributes data for a hang report. - /// - /// The hang report contribution. Null values should be ignored. - protected virtual HangReportContribution GetHangReport() + /// + /// Contributes data for a hang report. + /// + /// The hang report contribution. Null values should be ignored. + [RequiresUnreferencedCode(Reasons.DiagnosticAnalysisOnly)] + protected virtual HangReportContribution GetHangReport() + { + using (this.NoMessagePumpSynchronizationContext.Apply()) { - using (this.NoMessagePumpSynchronizationContext.Apply()) + lock (this.SyncContextLock) { - lock (this.SyncContextLock) - { - XDocument? dgml = CreateTemplateDgml(out XElement nodes, out XElement links); - - Dictionary? pendingTasksElements = this.CreateNodesForPendingTasks(); - List>? taskLabels = CreateNodeLabels(pendingTasksElements); - Dictionary? pendingTaskCollections = CreateNodesForJoinableTaskCollections(pendingTasksElements.Keys); - nodes.Add(pendingTasksElements.Values); - nodes.Add(pendingTaskCollections.Values); - nodes.Add(taskLabels.Select(t => t.Item1)); - links.Add(CreatesLinksBetweenNodes(pendingTasksElements)); - links.Add(CreateCollectionContainingTaskLinks(pendingTasksElements, pendingTaskCollections)); - links.Add(taskLabels.Select(t => t.Item2)); - - return new HangReportContribution( - dgml.ToString(), - "application/xml", - "JoinableTaskContext.dgml"); - } + XDocument? dgml = CreateTemplateDgml(out XElement nodes, out XElement links); + + Dictionary? pendingTasksElements = this.CreateNodesForPendingTasks(); + List>? taskLabels = CreateNodeLabels(pendingTasksElements); + Dictionary? pendingTaskCollections = CreateNodesForJoinableTaskCollections(pendingTasksElements.Keys); + nodes.Add(pendingTasksElements.Values); + nodes.Add(pendingTaskCollections.Values); + nodes.Add(taskLabels.Select(t => t.Item1)); + links.Add(CreatesLinksBetweenNodes(pendingTasksElements)); + links.Add(CreateCollectionContainingTaskLinks(pendingTasksElements, pendingTaskCollections)); + links.Add(taskLabels.Select(t => t.Item2)); + + return new HangReportContribution( + dgml.ToString(), + "application/xml", + "JoinableTaskContext.dgml"); } } + } - private static XDocument CreateTemplateDgml(out XElement nodes, out XElement links) - { - return Dgml.Create(out nodes, out links) - .WithCategories( - Dgml.Category("MainThreadBlocking", "Blocking main thread", background: "#FFF9FF7F", isTag: true), - Dgml.Category("NonEmptyQueue", "Non-empty queue", background: "#FFFF0000", isTag: true)); - } + private static XDocument CreateTemplateDgml(out XElement nodes, out XElement links) + { + return Dgml.Create(out nodes, out links) + .WithCategories( + Dgml.Category("MainThreadBlocking", "Blocking main thread", background: "#FFF9FF7F", isTag: true), + Dgml.Category("NonEmptyQueue", "Non-empty queue", background: "#FFFF0000", isTag: true)); + } - private static ICollection CreatesLinksBetweenNodes(Dictionary pendingTasksElements) - { - Requires.NotNull(pendingTasksElements, nameof(pendingTasksElements)); + private static ICollection CreatesLinksBetweenNodes(Dictionary pendingTasksElements) + { + Requires.NotNull(pendingTasksElements, nameof(pendingTasksElements)); - var links = new List(); - foreach (KeyValuePair joinableTaskAndElement in pendingTasksElements) + var links = new List(); + foreach (KeyValuePair joinableTaskAndElement in pendingTasksElements) + { + foreach (JoinableTask? joinedTask in JoinableTaskDependencyGraph.GetAllDirectlyDependentJoinableTasks(joinableTaskAndElement.Key)) { - foreach (JoinableTask? joinedTask in JoinableTaskDependencyGraph.GetAllDirectlyDependentJoinableTasks(joinableTaskAndElement.Key)) + if (pendingTasksElements.TryGetValue(joinedTask, out XElement? joinedTaskElement)) { - if (pendingTasksElements.TryGetValue(joinedTask, out XElement? joinedTaskElement)) - { - links.Add(Dgml.Link(joinableTaskAndElement.Value, joinedTaskElement)); - } + links.Add(Dgml.Link(joinableTaskAndElement.Value, joinedTaskElement)); } } - - return links; } - private static ICollection CreateCollectionContainingTaskLinks(Dictionary tasks, Dictionary collections) - { - Requires.NotNull(tasks, nameof(tasks)); - Requires.NotNull(collections, nameof(collections)); + return links; + } + + private static ICollection CreateCollectionContainingTaskLinks(Dictionary tasks, Dictionary collections) + { + Requires.NotNull(tasks, nameof(tasks)); + Requires.NotNull(collections, nameof(collections)); - var result = new List(); - foreach (KeyValuePair task in tasks) + var result = new List(); + foreach (KeyValuePair task in tasks) + { + foreach (JoinableTaskCollection? collection in task.Key.ContainingCollections) { - foreach (JoinableTaskCollection? collection in task.Key.ContainingCollections) - { - XElement? collectionElement = collections[collection]; - result.Add(Dgml.Link(collectionElement, task.Value).WithCategories("Contains")); - } + XElement? collectionElement = collections[collection]; + result.Add(Dgml.Link(collectionElement, task.Value).WithCategories("Contains")); } - - return result; } - private static Dictionary CreateNodesForJoinableTaskCollections(IEnumerable tasks) + return result; + } + + private static Dictionary CreateNodesForJoinableTaskCollections(IEnumerable tasks) + { + Requires.NotNull(tasks, nameof(tasks)); + + var collectionsSet = new HashSet(tasks.SelectMany(t => t.ContainingCollections)); + var result = new Dictionary(collectionsSet.Count); + int collectionId = 0; + foreach (JoinableTaskCollection? collection in collectionsSet) { - Requires.NotNull(tasks, nameof(tasks)); + collectionId++; + var label = string.IsNullOrEmpty(collection.DisplayName) ? "Collection #" + collectionId : collection.DisplayName; + XElement? element = Dgml.Node("Collection#" + collectionId, label, group: "Expanded") + .WithCategories("Collection"); + result.Add(collection, element); + } + + return result; + } + + [RequiresUnreferencedCode(Reasons.DiagnosticAnalysisOnly)] + private static List> CreateNodeLabels(Dictionary tasksAndElements) + { + Requires.NotNull(tasksAndElements, nameof(tasksAndElements)); - var collectionsSet = new HashSet(tasks.SelectMany(t => t.ContainingCollections)); - var result = new Dictionary(collectionsSet.Count); - int collectionId = 0; - foreach (JoinableTaskCollection? collection in collectionsSet) + var result = new List>(); + foreach (KeyValuePair tasksAndElement in tasksAndElements) + { + JoinableTask? pendingTask = tasksAndElement.Key; + XElement? node = tasksAndElement.Value; + int queueIndex = 0; + foreach (JoinableTaskFactory.SingleExecuteProtector? pendingTasksElement in pendingTask.MainThreadQueueContents) { - collectionId++; - var label = string.IsNullOrEmpty(collection.DisplayName) ? "Collection #" + collectionId : collection.DisplayName; - XElement? element = Dgml.Node("Collection#" + collectionId, label, group: "Expanded") - .WithCategories("Collection"); - result.Add(collection, element); + queueIndex++; + XElement? callstackNode = Dgml.Node(node.Attribute("Id")!.Value + "MTQueue#" + queueIndex, GetAsyncReturnStack(pendingTasksElement)); + XElement? callstackLink = Dgml.Link(callstackNode, node); + result.Add(Tuple.Create(callstackNode, callstackLink)); } - return result; + foreach (JoinableTaskFactory.SingleExecuteProtector? pendingTasksElement in pendingTask.ThreadPoolQueueContents) + { + queueIndex++; + XElement? callstackNode = Dgml.Node(node.Attribute("Id")!.Value + "TPQueue#" + queueIndex, GetAsyncReturnStack(pendingTasksElement)); + XElement? callstackLink = Dgml.Link(callstackNode, node); + result.Add(Tuple.Create(callstackNode, callstackLink)); + } } - private static List> CreateNodeLabels(Dictionary tasksAndElements) - { - Requires.NotNull(tasksAndElements, nameof(tasksAndElements)); + return result; + } - var result = new List>(); - foreach (KeyValuePair tasksAndElement in tasksAndElements) - { - JoinableTask? pendingTask = tasksAndElement.Key; - XElement? node = tasksAndElement.Value; - int queueIndex = 0; - foreach (JoinableTaskFactory.SingleExecuteProtector? pendingTasksElement in pendingTask.MainThreadQueueContents) - { - queueIndex++; - XElement? callstackNode = Dgml.Node(node.Attribute("Id").Value + "MTQueue#" + queueIndex, GetAsyncReturnStack(pendingTasksElement)); - XElement? callstackLink = Dgml.Link(callstackNode, node); - result.Add(Tuple.Create(callstackNode, callstackLink)); - } + [RequiresUnreferencedCode(Reasons.DiagnosticAnalysisOnly)] + private static string GetAsyncReturnStack(JoinableTaskFactory.SingleExecuteProtector singleExecuteProtector) + { + Requires.NotNull(singleExecuteProtector, nameof(singleExecuteProtector)); - foreach (JoinableTaskFactory.SingleExecuteProtector? pendingTasksElement in pendingTask.ThreadPoolQueueContents) - { - queueIndex++; - XElement? callstackNode = Dgml.Node(node.Attribute("Id").Value + "TPQueue#" + queueIndex, GetAsyncReturnStack(pendingTasksElement)); - XElement? callstackLink = Dgml.Link(callstackNode, node); - result.Add(Tuple.Create(callstackNode, callstackLink)); - } + var stringBuilder = new StringBuilder(); + try + { + foreach (var frame in singleExecuteProtector.WalkAsyncReturnStackFrames()) + { + stringBuilder.AppendLine(frame); } - - return result; } - - private static string GetAsyncReturnStack(JoinableTaskFactory.SingleExecuteProtector singleExecuteProtector) + catch (Exception ex) { - Requires.NotNull(singleExecuteProtector, nameof(singleExecuteProtector)); + // Just eat the exception so we don't crash during a hang report. + Report.Fail("GetAsyncReturnStackFrames threw exception: ", ex); + } - var stringBuilder = new StringBuilder(); - try + return stringBuilder.ToString().TrimEnd(); + } + + private Dictionary CreateNodesForPendingTasks() + { + var pendingTasksElements = new Dictionary(); + lock (this.pendingTasks) + { + int taskId = 0; + foreach (JoinableTask? pendingTask in this.pendingTasks) { - foreach (var frame in singleExecuteProtector.WalkAsyncReturnStackFrames()) + taskId++; + + string methodName = string.Empty; + System.Reflection.MethodInfo? entryMethodInfo = pendingTask.EntryMethodInfo; + if (entryMethodInfo is object) { - stringBuilder.AppendLine(frame); + methodName = string.Format( + CultureInfo.InvariantCulture, + " ({0}.{1})", + entryMethodInfo.DeclaringType?.FullName, + entryMethodInfo.Name); } - } - catch (Exception ex) - { - // Just eat the exception so we don't crash during a hang report. - Report.Fail("GetAsyncReturnStackFrames threw exception: ", ex); - } - return stringBuilder.ToString().TrimEnd(); - } + XElement? node = Dgml.Node("Task#" + taskId, "Task #" + taskId + methodName) + .WithCategories("Task"); + if (pendingTask.HasNonEmptyQueue) + { + node.WithCategories("NonEmptyQueue"); + } - private Dictionary CreateNodesForPendingTasks() - { - var pendingTasksElements = new Dictionary(); - lock (this.pendingTasks) - { - int taskId = 0; - foreach (JoinableTask? pendingTask in this.pendingTasks) + if (pendingTask.State.HasFlag(JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread)) { - taskId++; - - string methodName = string.Empty; - System.Reflection.MethodInfo? entryMethodInfo = pendingTask.EntryMethodInfo; - if (entryMethodInfo is object) - { - methodName = string.Format( - CultureInfo.InvariantCulture, - " ({0}.{1})", - entryMethodInfo.DeclaringType?.FullName, - entryMethodInfo.Name); - } - - XElement? node = Dgml.Node("Task#" + taskId, "Task #" + taskId + methodName) - .WithCategories("Task"); - if (pendingTask.HasNonEmptyQueue) - { - node.WithCategories("NonEmptyQueue"); - } - - if (pendingTask.State.HasFlag(JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread)) - { - node.WithCategories("MainThreadBlocking"); - } - - pendingTasksElements.Add(pendingTask, node); + node.WithCategories("MainThreadBlocking"); } - } - return pendingTasksElements; + pendingTasksElements.Add(pendingTask, node); + } } + + return pendingTasksElements; } } diff --git a/src/Microsoft.VisualStudio.Threading/JoinableTaskContext.cs b/src/Microsoft.VisualStudio.Threading/JoinableTaskContext.cs index 1a1f98145..3baa6cbae 100644 --- a/src/Microsoft.VisualStudio.Threading/JoinableTaskContext.cs +++ b/src/Microsoft.VisualStudio.Threading/JoinableTaskContext.cs @@ -1,706 +1,969 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using static System.FormattableString; +using JoinableTaskSynchronizationContext = Microsoft.VisualStudio.Threading.JoinableTask.JoinableTaskSynchronizationContext; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// A common context within which joinable tasks may be created and interact to avoid deadlocks. +/// +/// +/// There are three rules that should be strictly followed when using or interacting +/// with JoinableTasks: +/// 1. If a method has certain thread apartment requirements (STA or MTA) it must either: +/// a) Have an asynchronous signature, and asynchronously marshal to the appropriate +/// thread if it isn't originally invoked on a compatible thread. +/// The recommended way to switch to the main thread is: +/// +/// await JoinableTaskFactory.SwitchToMainThreadAsync(); +/// +/// b) Have a synchronous signature, and throw an exception when called on the wrong thread. +/// In particular, no method is allowed to synchronously marshal work to another thread +/// (blocking while that work is done). Synchronous blocks in general are to be avoided +/// whenever possible. +/// 2. When an implementation of an already-shipped public API must call asynchronous code +/// and block for its completion, it must do so by following this simple pattern: +/// +/// JoinableTaskFactory.Run(async delegate { +/// await SomeOperationAsync(...); +/// }); +/// +/// 3. If ever awaiting work that was started earlier, that work must be Joined. +/// For example, one service kicks off some asynchronous work that may later become +/// synchronously blocking: +/// +/// JoinableTask longRunningAsyncWork = JoinableTaskFactory.RunAsync(async delegate { +/// await SomeOperationAsync(...); +/// }); +/// +/// Then later that async work becomes blocking: +/// +/// longRunningAsyncWork.Join(); +/// +/// or perhaps: +/// +/// await longRunningAsyncWork; +/// +/// Note however that this extra step is not necessary when awaiting is done +/// immediately after kicking off an asynchronous operation. +/// +public partial class JoinableTaskContext : IDisposable { - using System; - using System.Collections.Concurrent; - using System.Collections.Generic; - using System.Collections.Specialized; - using System.Diagnostics; - using System.Linq; - using System.Reflection; - using System.Runtime.CompilerServices; - using System.Threading; - using System.Threading.Tasks; - using JoinableTaskSynchronizationContext = Microsoft.VisualStudio.Threading.JoinableTask.JoinableTaskSynchronizationContext; - using SingleExecuteProtector = Microsoft.VisualStudio.Threading.JoinableTaskFactory.SingleExecuteProtector; - - /// - /// A common context within which joinable tasks may be created and interact to avoid deadlocks. - /// - /// - /// Lots of documentation and FAQ on Joinable Tasks is available on OneNote: . - /// + /// + /// The expected length of the serialized task ID. + /// /// - /// There are three rules that should be strictly followed when using or interacting - /// with JoinableTasks: - /// 1. If a method has certain thread apartment requirements (STA or MTA) it must either: - /// a) Have an asynchronous signature, and asynchronously marshal to the appropriate - /// thread if it isn't originally invoked on a compatible thread. - /// The recommended way to switch to the main thread is: - /// - /// await JoinableTaskFactory.SwitchToMainThreadAsync(); - /// - /// b) Have a synchronous signature, and throw an exception when called on the wrong thread. - /// In particular, no method is allowed to synchronously marshal work to another thread - /// (blocking while that work is done). Synchronous blocks in general are to be avoided - /// whenever possible. - /// 2. When an implementation of an already-shipped public API must call asynchronous code - /// and block for its completion, it must do so by following this simple pattern: - /// - /// JoinableTaskFactory.Run(async delegate { - /// await SomeOperationAsync(...); - /// }); - /// - /// 3. If ever awaiting work that was started earlier, that work must be Joined. - /// For example, one service kicks off some asynchronous work that may later become - /// synchronously blocking: - /// - /// JoinableTask longRunningAsyncWork = JoinableTaskFactory.RunAsync(async delegate { - /// await SomeOperationAsync(...); - /// }); - /// - /// Then later that async work becomes blocking: - /// - /// longRunningAsyncWork.Join(); - /// - /// or perhaps: - /// - /// await longRunningAsyncWork; - /// - /// Note however that this extra step is not necessary when awaiting is done - /// immediately after kicking off an asynchronous operation. + /// This value is the length required to hex-encode a 64-bit integer. We use for task IDs, so this is appropriate. /// - public partial class JoinableTaskContext : IDisposable - { - /// - /// A "global" lock that allows the graph of interconnected sync context and JoinableSet instances - /// communicate in a thread-safe way without fear of deadlocks due to each taking their own private - /// lock and then calling others, thus leading to deadlocks from lock ordering issues. - /// - /// - /// Yes, global locks should be avoided wherever possible. However even MEF from the .NET Framework - /// uses a global lock around critical composition operations because containers can be interconnected - /// in arbitrary ways. The code in this file has a very similar problem, so we use a similar solution. - /// Except that our lock is only as global as the JoinableTaskContext. It isn't static. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private readonly object syncContextLock = new object(); + private const int TaskIdHexLength = 16; - /// - /// An AsyncLocal value that carries the joinable instance associated with an async operation. - /// - private readonly AsyncLocal> joinableOperation = new AsyncLocal>(); + /// + /// A "global" lock that allows the graph of interconnected sync context and JoinableSet instances + /// communicate in a thread-safe way without fear of deadlocks due to each taking their own private + /// lock and then calling others, thus leading to deadlocks from lock ordering issues. + /// + /// + /// Yes, global locks should be avoided wherever possible. However even MEF from the .NET Framework + /// uses a global lock around critical composition operations because containers can be interconnected + /// in arbitrary ways. The code in this file has a very similar problem, so we use a similar solution. + /// Except that our lock is only as global as the JoinableTaskContext. It isn't static. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly object syncContextLock = new object(); - /// - /// The set of tasks that have started but have not yet completed. - /// - /// - /// All access to this collection should be guarded by locking this collection. - /// - private readonly HashSet pendingTasks = new HashSet(); + /// + /// An AsyncLocal value that carries the joinable instance associated with an async operation. + /// + private readonly AsyncLocal> joinableOperation = new AsyncLocal>(); - /// - /// The stack of tasks which synchronously blocks the main thread in the initial stage (before it yields and CompleteOnCurrentThread starts.) - /// - /// - /// Normally we expect this stack contains 0 or 1 task. When a synchronous task starts another synchronous task in the initialization stage, - /// we might get more than 1 tasks, but it should be very rare to exceed 2 tasks. - /// All access to this collection should be guarded by locking this collection. - /// - private readonly Stack initializingSynchronouslyMainThreadTasks = new Stack(2); + /// + /// The set of tasks that have started but have not yet completed. + /// + /// + /// All access to this collection should be guarded by locking this collection. + /// + private readonly HashSet pendingTasks = new HashSet(); - /// - /// A set of receivers of hang notifications. - /// - /// - /// All access to this collection should be guarded by locking this collection. - /// - private readonly HashSet hangNotifications = new HashSet(); + /// + /// The stack of tasks which synchronously blocks the main thread in the initial stage (before it yields and CompleteOnCurrentThread starts.) + /// + /// + /// Normally we expect this stack contains 0 or 1 task. When a synchronous task starts another synchronous task in the initialization stage, + /// we might get more than 1 tasks, but it should be very rare to exceed 2 tasks. + /// All access to this collection should be guarded by locking this collection. + /// + private readonly Stack initializingSynchronouslyMainThreadTasks = new Stack(2); - /// - /// The ManagedThreadID for the main thread. - /// - private readonly int mainThreadManagedThreadId; + /// + /// A set of receivers of hang notifications. + /// + /// + /// All access to this collection should be guarded by locking this collection. + /// + private readonly HashSet hangNotifications = new HashSet(); - /// - /// A single joinable task factory that itself cannot be joined. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private JoinableTaskFactory? nonJoinableFactory; + /// + /// The ManagedThreadID for the main thread. + /// + private readonly int mainThreadManagedThreadId; - /// - /// Initializes a new instance of the class - /// assuming the current thread is the main thread and - /// will provide the means to switch - /// to the main thread from another thread. - /// - public JoinableTaskContext() - : this(Thread.CurrentThread, SynchronizationContext.Current) - { - } + /// + /// A dictionary of incomplete objects for which serializable identifiers have been requested. + /// + /// + /// Only access this while locking . + /// + private readonly Dictionary serializedTasks = new(); - /// - /// Initializes a new instance of the class. - /// - /// - /// The thread to switch to in . - /// If omitted, the current thread will be assumed to be the main thread. - /// - /// - /// The synchronization context to use to switch to the main thread. - /// - public JoinableTaskContext(Thread? mainThread = null, SynchronizationContext? synchronizationContext = null) - { - this.MainThread = mainThread ?? Thread.CurrentThread; - this.mainThreadManagedThreadId = this.MainThread.ManagedThreadId; - this.UnderlyingSynchronizationContext = synchronizationContext ?? SynchronizationContext.Current; // may still be null after this. - } + /// + /// A unique instance ID that is used when creating IDs for JoinableTasks that come from this instance. + /// + private readonly string contextId = Guid.NewGuid().ToString("n"); - /// - /// Gets the factory which creates joinable tasks - /// that do not belong to a joinable task collection. - /// - public JoinableTaskFactory Factory + /// + /// The next unique ID to assign to a for which a token is required. + /// + private ulong nextTaskId = 1; + + /// + /// The count of s blocking the main thread. + /// + private volatile int mainThreadBlockingJoinableTaskCount; + + /// + /// A single joinable task factory that itself cannot be joined. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private JoinableTaskFactory? nonJoinableFactory; + + /// + /// Initializes a new instance of the class + /// assuming the current thread is the main thread and + /// will provide the means to switch + /// to the main thread from another thread. + /// + /// + /// + /// When is at the time this constructor is invoked, + /// requests to switch to the main thread using + /// will not result in any thread switch. + /// This is appropriate for unit test environments where there is no main thread to switch to or processes + /// which otherwise do not define a main thread. + /// Thread safety concern: When configured without a synchronization context, code that requests the main thread + /// as a means of avoiding concurrency may malfunction due to data race conditions. + /// + /// + public JoinableTaskContext() + : this(Thread.CurrentThread, SynchronizationContext.Current) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The thread to switch to in . + /// If , the current thread will be assumed to be the main thread. + /// + /// + /// The synchronization context to use to switch to the main thread. + /// + /// If is specified (or the argument is omitted), the current synchronization context will be used. + /// If is also , + /// requests to switch to the main thread using will not result in any thread switch. + /// This is appropriate for unit test environments where there is no main thread to switch to or processes + /// which otherwise do not define a main thread. + /// Thread safety concern: When configured without a synchronization context, code that requests the main thread + /// as a means of avoiding concurrency may malfunction due to data race conditions. + /// + /// + public JoinableTaskContext(Thread? mainThread = null, SynchronizationContext? synchronizationContext = null) + { + this.MainThread = mainThread ?? Thread.CurrentThread; + this.mainThreadManagedThreadId = this.MainThread.ManagedThreadId; + this.UnderlyingSynchronizationContext = synchronizationContext ?? SynchronizationContext.Current; // may still be null after this. + } + + /// + /// Gets the factory which creates joinable tasks + /// that do not belong to a joinable task collection. + /// + public JoinableTaskFactory Factory + { + get { - get + if (this.nonJoinableFactory is null) { - if (this.nonJoinableFactory is null) + using (this.NoMessagePumpSynchronizationContext.Apply()) { - using (this.NoMessagePumpSynchronizationContext.Apply()) + lock (this.SyncContextLock) { - lock (this.SyncContextLock) + if (this.nonJoinableFactory is null) { - if (this.nonJoinableFactory is null) - { - this.nonJoinableFactory = this.CreateDefaultFactory(); - } + this.nonJoinableFactory = this.CreateDefaultFactory(); } } } - - return this.nonJoinableFactory; } + + return this.nonJoinableFactory; } + } - /// - /// Gets the main thread that can be shared by tasks created by this context. - /// - public Thread MainThread { get; private set; } + /// + /// Gets the main thread that can be shared by tasks created by this context. + /// + public Thread MainThread { get; private set; } - /// - /// Gets a value indicating whether the caller is executing on the main thread. - /// - public bool IsOnMainThread => Environment.CurrentManagedThreadId == this.mainThreadManagedThreadId; + /// + /// Gets a value indicating whether the caller is executing on the main thread. + /// + public bool IsOnMainThread => Environment.CurrentManagedThreadId == this.mainThreadManagedThreadId; - /// - /// Gets a value indicating whether the caller is currently running within the context of a joinable task. - /// - /// - /// Use of this property is generally discouraged, as any operation that becomes a no-op when no - /// ambient JoinableTask is present is very cheap. For clients that have complex algorithms that are - /// only relevant if an ambient joinable task is present, this property may serve to skip that for - /// performance reasons. - /// - public bool IsWithinJoinableTask - { - get { return this.AmbientTask is object; } - } + /// + /// Gets a value indicating whether the caller is currently running within the context of a joinable task. + /// + /// + /// Use of this property is generally discouraged, as any operation that becomes a no-op when no + /// ambient JoinableTask is present is very cheap. For clients that have complex algorithms that are + /// only relevant if an ambient joinable task is present, this property may serve to skip that for + /// performance reasons. + /// + public bool IsWithinJoinableTask + { + get { return this.AmbientTask is object; } + } - /// - /// Gets the underlying that controls the main thread in the host. - /// - internal SynchronizationContext? UnderlyingSynchronizationContext { get; private set; } + /// + /// Gets a value indicating whether this instance is not associated with any main thread + /// (e.g. created with ). + /// + /// + /// This allows library code to skip some additional work in the environments that do not have a main thread. + /// + public bool IsNoOpContext => this.UnderlyingSynchronizationContext is null; - /// - /// Gets the context-wide synchronization lock. - /// - internal object SyncContextLock - { - get { return this.syncContextLock; } - } + /// + /// Gets a value indicating whether the main thread is blocked by any joinable task. + /// + internal bool IsMainThreadBlockedByAnyJoinableTask => this.mainThreadBlockingJoinableTaskCount > 0; - /// - /// Gets or sets the caller's ambient joinable task. - /// - internal JoinableTask? AmbientTask - { - get - { - JoinableTask? result = null; - this.joinableOperation.Value?.TryGetTarget(out result); - return result; - } + /// + /// Gets the underlying that controls the main thread in the host. + /// + internal SynchronizationContext? UnderlyingSynchronizationContext { get; private set; } - set => this.joinableOperation.Value = value?.WeakSelf; - } + /// + /// Gets the context-wide synchronization lock. + /// + internal object SyncContextLock + { + get { return this.syncContextLock; } + } - /// - /// Gets a which, when applied, - /// suppresses any message pump that may run during synchronous blocks - /// of the calling thread. - /// - /// - /// The default implementation of this property is effective - /// in builds of this assembly that target the .NET Framework. - /// But on builds that target the portable profile, it should be - /// overridden to provide an effective platform-specific solution. - /// - protected internal virtual SynchronizationContext NoMessagePumpSynchronizationContext + /// + /// Gets or sets the caller's ambient joinable task. + /// + internal JoinableTask? AmbientTask + { + get { - get - { - // Callers of this method are about to take a private lock, which tends - // to cause a deadlock while debugging because of lock contention with the - // debugger's expression evaluator. So prevent that. - Debugger.NotifyOfCrossThreadDependency(); + JoinableTask? result = null; + this.joinableOperation.Value?.TryGetTarget(out result); + return result; + } - return NoMessagePumpSyncContext.Default; - } + set => this.joinableOperation.Value = value?.WeakSelf; + } + + /// + /// Gets a which, when applied, + /// suppresses any message pump that may run during synchronous blocks + /// of the calling thread. + /// + /// + /// The default implementation of this property is effective + /// in builds of this assembly that target the .NET Framework. + /// But on builds that target the portable profile, it should be + /// overridden to provide an effective platform-specific solution. + /// + protected internal virtual SynchronizationContext NoMessagePumpSynchronizationContext + { + get + { + return NoMessagePumpSyncContext.Default; } + } - /// - /// Conceals any JoinableTask the caller is associated with until the returned value is disposed. - /// - /// A value to dispose of to restore visibility into the caller's associated JoinableTask, if any. - /// - /// In some cases asynchronous work may be spun off inside a delegate supplied to Run, - /// so that the work does not have privileges to re-enter the Main thread until the - /// call has returned and the UI thread is idle. - /// To prevent the asynchronous work from automatically being allowed to re-enter the Main thread, - /// wrap the code that calls the asynchronous task in a using block with a call to this method - /// as the expression. - /// - /// - /// this.JoinableTaskContext.RunSynchronously(async delegate { - /// using(this.JoinableTaskContext.SuppressRelevance()) { - /// var asyncOperation = Task.Run(async delegate { - /// // Some background work. - /// await this.JoinableTaskContext.SwitchToMainThreadAsync(); - /// // Some Main thread work, that cannot begin until the outer RunSynchronously call has returned. - /// }); - /// } - /// - /// // Because the asyncOperation is not related to this Main thread work (it was suppressed), - /// // the following await *would* deadlock if it were uncommented. - /// ////await asyncOperation; - /// }); - /// - /// - /// - public RevertRelevance SuppressRelevance() + /// + /// Initializes a new instance of the class + /// that is configured to no-op on calls to . + /// + /// A new instance of . + /// + /// + /// This method is equivalent to calling the constructor + /// with the property first set to . + /// This entry point however will have the same behavior regardless of the value of . + /// + /// + /// The caller's thread will still be captured for use by such properties as + /// and . + /// These properties generally have no effect except as used by application-specific code beyond this library. + /// + /// + /// This method is useful for creating a in a unit test environment + /// or in a process that does not have a main thread, but which includes code that requires an instance + /// of or . + /// Such code can receive the instance returned by this method and use it in a normal way but no main thread switches will be honored. + /// + /// + /// Thread safety concern: Because main thread switches will not be honored, code that requests the main thread + /// as a means of avoiding concurrency may malfunction due to data race conditions. + /// + /// + public static JoinableTaskContext CreateNoOpContext() + { + using (((SynchronizationContext?)null).Apply()) { - return new RevertRelevance(this); + return new JoinableTaskContext(); } + } - /// - /// Gets a value indicating whether the main thread is blocked for the caller's completion. - /// - public bool IsMainThreadBlocked() + /// + /// Conceals any JoinableTask the caller is associated with until the returned value is disposed. + /// + /// A value to dispose of to restore visibility into the caller's associated JoinableTask, if any. + /// + /// In some cases asynchronous work may be spun off inside a delegate supplied to Run, + /// so that the work does not have privileges to re-enter the Main thread until the + /// call has returned and the UI thread is idle. + /// To prevent the asynchronous work from automatically being allowed to re-enter the Main thread, + /// wrap the code that calls the asynchronous task in a using block with a call to this method + /// as the expression. + /// + /// + /// this.JoinableTaskContext.RunSynchronously(async delegate { + /// using(this.JoinableTaskContext.SuppressRelevance()) { + /// var asyncOperation = Task.Run(async delegate { + /// // Some background work. + /// await this.JoinableTaskContext.SwitchToMainThreadAsync(); + /// // Some Main thread work, that cannot begin until the outer RunSynchronously call has returned. + /// }); + /// } + /// + /// // Because the asyncOperation is not related to this Main thread work (it was suppressed), + /// // the following await *would* deadlock if it were uncommented. + /// ////await asyncOperation; + /// }); + /// + /// + /// + public RevertRelevance SuppressRelevance() + { + return new RevertRelevance(this); + } + + /// + /// Gets a value indicating whether the main thread is blocked for the caller's completion. + /// + public bool IsMainThreadBlocked() + { + JoinableTask? ambientTask = this.AmbientTask; + if (ambientTask is object) { - JoinableTask? ambientTask = this.AmbientTask; - if (ambientTask is object) + if (JoinableTaskDependencyGraph.HasMainThreadSynchronousTaskWaiting(ambientTask)) { - if (JoinableTaskDependencyGraph.HasMainThreadSynchronousTaskWaiting(ambientTask)) - { - return true; - } + return true; + } - // The JoinableTask dependent chain gives a fast way to check IsMainThreadBlocked. - // However, it only works when the main thread tasks is in the CompleteOnCurrentThread loop. - // The dependent chain won't be added when a synchronous task is in the initialization phase. - // In that case, we still need to follow the descendent of the task in the initialization stage. - // We hope the dependency tree is relatively small in that stage. - using (this.NoMessagePumpSynchronizationContext.Apply()) + // The JoinableTask dependent chain gives a fast way to check IsMainThreadBlocked. + // However, it only works when the main thread tasks is in the CompleteOnCurrentThread loop. + // The dependent chain won't be added when a synchronous task is in the initialization phase. + // In that case, we still need to follow the descendent of the task in the initialization stage. + // We hope the dependency tree is relatively small in that stage. + using (this.NoMessagePumpSynchronizationContext.Apply()) + { + lock (this.SyncContextLock) { - lock (this.SyncContextLock) + lock (this.initializingSynchronouslyMainThreadTasks) { - lock (this.initializingSynchronouslyMainThreadTasks) + if (this.initializingSynchronouslyMainThreadTasks.Count > 0) { - if (this.initializingSynchronouslyMainThreadTasks.Count > 0) + // our read lock doesn't cover this collection + var allJoinedJobs = new HashSet(); + foreach (JoinableTask? initializingTask in this.initializingSynchronouslyMainThreadTasks) { - // our read lock doesn't cover this collection - var allJoinedJobs = new HashSet(); - foreach (JoinableTask? initializingTask in this.initializingSynchronouslyMainThreadTasks) + if (!JoinableTaskDependencyGraph.HasMainThreadSynchronousTaskWaiting(initializingTask)) { - if (!JoinableTaskDependencyGraph.HasMainThreadSynchronousTaskWaiting(initializingTask)) + // This task blocks the main thread. If it has joined the ambient task + // directly or indirectly, then our ambient task is considered blocking + // the main thread. + JoinableTaskDependencyGraph.AddSelfAndDescendentOrJoinedJobs(initializingTask, allJoinedJobs); + if (allJoinedJobs.Contains(ambientTask)) { - // This task blocks the main thread. If it has joined the ambient task - // directly or indirectly, then our ambient task is considered blocking - // the main thread. - JoinableTaskDependencyGraph.AddSelfAndDescendentOrJoinedJobs(initializingTask, allJoinedJobs); - if (allJoinedJobs.Contains(ambientTask)) - { - return true; - } - - allJoinedJobs.Clear(); + return true; } + + allJoinedJobs.Clear(); } } } } } } + } + + return false; + } - return false; + /// + /// Gets a very likely value whether the main thread is blocked for the caller's completion. + /// It is less accurate when the UI thread blocking task just starts and hasn't been blocked yet, or the dependency chain is just removed. + /// However, unlike , this implementation is lock free, and faster in high contention scenarios. + /// + public bool IsMainThreadMaybeBlocked() + { + JoinableTask? ambientTask = this.AmbientTask; + if (ambientTask is object) + { + return ambientTask.MaybeBlockMainThread(); } - /// - /// Gets a very likely value whether the main thread is blocked for the caller's completion. - /// It is less accurate when the UI thread blocking task just starts and hasn't been blocked yet, or the dependency chain is just removed. - /// However, unlike , this implementation is lock free, and faster in high contention scenarios. - /// - public bool IsMainThreadMaybeBlocked() + return false; + } + + /// + /// Creates a joinable task factory that automatically adds all created tasks + /// to a collection that can be jointly joined. + /// + /// The collection that all tasks should be added to. + public virtual JoinableTaskFactory CreateFactory(JoinableTaskCollection collection) + { + Requires.NotNull(collection, nameof(collection)); + return new JoinableTaskFactory(collection); + } + + /// + /// Creates a collection for in-flight joinable tasks. + /// + /// A new joinable task collection. + public JoinableTaskCollection CreateCollection() + { + return new JoinableTaskCollection(this); + } + + /// + /// Captures the caller's context and serializes it as a string + /// that is suitable for application via a subsequent call to . + /// + /// A string that represent the current context, or if there is none. + /// + /// To optimize calling patterns, this method returns even when inside a context + /// when this was initialized without a , which means no main thread exists + /// and thus there is no need to capture and reapply tokens. + /// + public string? Capture() => this.UnderlyingSynchronizationContext is null ? null : this.AmbientTask?.GetSerializableToken(); + + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Raised when a joinable task starts. + /// + /// The task that has started. + internal void OnJoinableTaskStarted(JoinableTask task) + { + Requires.NotNull(task, nameof(task)); + + using (this.NoMessagePumpSynchronizationContext.Apply()) { - JoinableTask? ambientTask = this.AmbientTask; - if (ambientTask is object) + lock (this.pendingTasks) + { + Assumes.True(this.pendingTasks.Add(task)); + } + + if ((task.State & JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread) == JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread) { - if ((ambientTask.State & JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread) == JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread) + lock (this.initializingSynchronouslyMainThreadTasks) { - return true; + this.initializingSynchronouslyMainThreadTasks.Push(task); } - - return JoinableTaskDependencyGraph.MaybeHasMainThreadSynchronousTaskWaiting(ambientTask); } - - return false; } + } - /// - /// Creates a joinable task factory that automatically adds all created tasks - /// to a collection that can be jointly joined. - /// - /// The collection that all tasks should be added to. - public virtual JoinableTaskFactory CreateFactory(JoinableTaskCollection collection) - { - Requires.NotNull(collection, nameof(collection)); - return new JoinableTaskFactory(collection); - } + /// + /// Raised when a joinable task completes. + /// + /// The completing task. + internal void OnJoinableTaskCompleted(JoinableTask task) + { + Requires.NotNull(task, nameof(task)); - /// - /// Creates a collection for in-flight joinable tasks. - /// - /// A new joinable task collection. - public JoinableTaskCollection CreateCollection() + using (this.NoMessagePumpSynchronizationContext.Apply()) { - return new JoinableTaskCollection(this); + lock (this.pendingTasks) + { + this.pendingTasks.Remove(task); + } } + } - /// - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } + /// + /// Raised when it starts to wait a joinable task to complete in the main thread. + /// + /// The task requires to be completed. + internal void OnSynchronousJoinableTaskToCompleteOnMainThread(JoinableTask task) + { + Requires.NotNull(task, nameof(task)); - /// - /// Raised when a joinable task starts. - /// - /// The task that has started. - internal void OnJoinableTaskStarted(JoinableTask task) + using (this.NoMessagePumpSynchronizationContext.Apply()) { - Requires.NotNull(task, nameof(task)); - - using (this.NoMessagePumpSynchronizationContext.Apply()) + lock (this.initializingSynchronouslyMainThreadTasks) { - lock (this.pendingTasks) - { - Assumes.True(this.pendingTasks.Add(task)); - } - - if ((task.State & JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread) == JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread) - { - lock (this.initializingSynchronouslyMainThreadTasks) - { - this.initializingSynchronouslyMainThreadTasks.Push(task); - } - } + Assumes.True(this.initializingSynchronouslyMainThreadTasks.Count > 0); + Assumes.True(this.initializingSynchronouslyMainThreadTasks.Peek() == task); + this.initializingSynchronouslyMainThreadTasks.Pop(); } } + } - /// - /// Raised when a joinable task completes. - /// - /// The completing task. - internal void OnJoinableTaskCompleted(JoinableTask task) - { - Requires.NotNull(task, nameof(task)); + /// + /// Registers a node for notification when a hang is detected. + /// + /// The instance to notify. + /// A value to dispose of to cancel registration. + internal IDisposable RegisterHangNotifications(JoinableTaskContextNode node) + { + Requires.NotNull(node, nameof(node)); - using (this.NoMessagePumpSynchronizationContext.Apply()) + using (this.NoMessagePumpSynchronizationContext.Apply()) + { + lock (this.hangNotifications) { - lock (this.pendingTasks) + if (!this.hangNotifications.Add(node)) { - this.pendingTasks.Remove(task); + Verify.FailOperation(Strings.JoinableTaskContextNodeAlreadyRegistered); } } } - /// - /// Raised when it starts to wait a joinable task to complete in the main thread. - /// - /// The task requires to be completed. - internal void OnSynchronousJoinableTaskToCompleteOnMainThread(JoinableTask task) - { - Requires.NotNull(task, nameof(task)); + return new HangNotificationRegistration(node); + } - using (this.NoMessagePumpSynchronizationContext.Apply()) + /// + /// Increment the count of s blocking the main thread. + /// + /// + /// This method should only be called on the main thread. + /// + internal void IncrementMainThreadBlockingCount() + { + Assumes.True(this.IsOnMainThread); + this.mainThreadBlockingJoinableTaskCount++; + } + + /// + /// Decrement the count of s blocking the main thread. + /// + /// + /// This method should only be called on the main thread. + /// + internal void DecrementMainThreadBlockingCount() + { + Assumes.True(this.IsOnMainThread); + this.mainThreadBlockingJoinableTaskCount--; + } + + /// + /// Reserves a unique ID for the given and records the association in the table. + /// + /// The to associate with the new ID. + /// + /// An ID assignment that is unique for this . + /// It must be passed to when the task completes to avoid a memory leak. + /// + internal ulong AssignUniqueIdentifier(JoinableTask joinableTask) + { + // The caller must have entered this lock because it's required that it only do it while it has not completed + // so that we don't have a leak in our dictionary, since completion removes the id's from the dictionary. + Assumes.True(Monitor.IsEntered(this.SyncContextLock)); + ulong taskId = checked(this.nextTaskId++); + this.serializedTasks.Add(taskId, joinableTask); + return taskId; + } + + /// + /// Applies the result of a call to to the caller's context. + /// + /// The result of a prior call. + /// The task referenced by the parent token if it came from our context and is still running; otherwise . + internal JoinableTask? Lookup(string? parentToken) + { + if (parentToken is not null) + { +#if NET + ReadOnlySpan taskIdChars = this.GetOurTaskId(parentToken); +#else + string taskIdChars = this.GetOurTaskId(parentToken.AsSpan()).ToString(); +#endif + if (ulong.TryParse(taskIdChars, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out ulong taskId)) { - lock (this.initializingSynchronouslyMainThreadTasks) + using (this.NoMessagePumpSynchronizationContext.Apply()) { - Assumes.True(this.initializingSynchronouslyMainThreadTasks.Count > 0); - Assumes.True(this.initializingSynchronouslyMainThreadTasks.Peek() == task); - this.initializingSynchronouslyMainThreadTasks.Pop(); + lock (this.SyncContextLock) + { + if (this.serializedTasks.TryGetValue(taskId, out JoinableTask? deserialized)) + { + return deserialized; + } + } } } } - /// - /// Registers a node for notification when a hang is detected. - /// - /// The instance to notify. - /// A value to dispose of to cancel registration. - internal IDisposable RegisterHangNotifications(JoinableTaskContextNode node) + return null; + } + + /// + /// Assembles a new token based on a parent token and the unique ID for some . + /// + /// The value previously obtained from . + /// The parent token the was created with, if any. + /// A token that may be serialized to recreate the dependency chain for this and its remote parents. + internal string ConstructFullToken(ulong taskId, string? parentToken) + { + const char ContextAndTaskSeparator = ':'; + if (parentToken is null) { - Requires.NotNull(node, nameof(node)); + return Invariant($"{this.contextId}{ContextAndTaskSeparator}{taskId:X16}"); + } + else + { + const char ContextSeparator = ';'; - using (this.NoMessagePumpSynchronizationContext.Apply()) + StringBuilder builder = new(parentToken.Length + 1 + this.contextId.Length + 1 + TaskIdHexLength); + builder.Append(parentToken); + + string taskIdString = taskId.ToString("X16", CultureInfo.InvariantCulture); + + // Replace our own contextual unique ID if it is found in the parent token. + int ownTaskIdIndex = this.FindOurTaskId(parentToken.AsSpan()); + if (ownTaskIdIndex < 0) { - lock (this.hangNotifications) - { - if (!this.hangNotifications.Add(node)) - { - Verify.FailOperation(Strings.JoinableTaskContextNodeAlreadyRegistered); - } - } + // Add our own task ID because we have no presence in the parent token already. + builder.Append(ContextSeparator); + builder.Append(this.contextId); + builder.Append(ContextAndTaskSeparator); + builder.Append(taskIdString); + } + else + { + // Replace our existing task ID that appears in the parent token. + builder.Remove(ownTaskIdIndex, TaskIdHexLength); + builder.Insert(ownTaskIdIndex, taskIdString); } - return new HangNotificationRegistration(node); + return builder.ToString(); } + } - /// - /// Invoked when a hang is suspected to have occurred involving the main thread. - /// - /// The duration of the current hang. - /// The number of times this hang has been reported, including this one. - /// A random GUID that uniquely identifies this particular hang. - /// - /// A single hang occurrence may invoke this method multiple times, with increasing - /// values in the parameter. - /// - protected internal virtual void OnHangDetected(TimeSpan hangDuration, int notificationCount, Guid hangId) + /// + /// Removes an association between a and a unique ID that was generated for it + /// from the table. + /// + /// The value previously obtained from . + /// + /// This method must be called when a is completed to avoid a memory leak. + /// + internal void RemoveSerializableIdentifier(ulong taskId) + { + Assumes.True(Monitor.IsEntered(this.SyncContextLock)); + Assumes.True(this.serializedTasks.Remove(taskId)); + } + + /// + /// Invoked when a hang is suspected to have occurred involving the main thread. + /// + /// The duration of the current hang. + /// The number of times this hang has been reported, including this one. + /// A random GUID that uniquely identifies this particular hang. + /// + /// A single hang occurrence may invoke this method multiple times, with increasing + /// values in the parameter. + /// + protected internal virtual void OnHangDetected(TimeSpan hangDuration, int notificationCount, Guid hangId) + { + List listeners; + using (this.NoMessagePumpSynchronizationContext.Apply()) { - List listeners; - using (this.NoMessagePumpSynchronizationContext.Apply()) + lock (this.hangNotifications) { - lock (this.hangNotifications) - { - listeners = this.hangNotifications.ToList(); - } + listeners = this.hangNotifications.ToList(); } + } - JoinableTask? blockingTask = JoinableTask.TaskCompletingOnThisThread; - var hangDetails = new HangDetails( - hangDuration, - notificationCount, - hangId, - blockingTask?.EntryMethodInfo); - foreach (JoinableTaskContextNode? listener in listeners) + JoinableTask? blockingTask = JoinableTask.TaskCompletingOnThisThread; + var hangDetails = new HangDetails( + hangDuration, + notificationCount, + hangId, + blockingTask?.EntryMethodInfo); + foreach (JoinableTaskContextNode? listener in listeners) + { + try { - try - { - listener.OnHangDetected(hangDetails); - } - catch (Exception ex) - { - // Report it in CHK, but don't throw. In a hang situation, we don't want the product - // to fail for another reason, thus hiding the hang issue. - Report.Fail("Exception thrown from OnHangDetected listener. {0}", ex); - } + listener.OnHangDetected(hangDetails); + } + catch (Exception ex) + { + // Report it in CHK, but don't throw. In a hang situation, we don't want the product + // to fail for another reason, thus hiding the hang issue. + Report.Fail("Exception thrown from OnHangDetected listener. {0}", ex); } } + } - /// - /// Invoked when an earlier hang report is false alarm. - /// - protected internal virtual void OnFalseHangDetected(TimeSpan hangDuration, Guid hangId) + /// + /// Invoked when an earlier hang report is false alarm. + /// + protected internal virtual void OnFalseHangDetected(TimeSpan hangDuration, Guid hangId) + { + List listeners; + using (this.NoMessagePumpSynchronizationContext.Apply()) { - List listeners; - using (this.NoMessagePumpSynchronizationContext.Apply()) + lock (this.hangNotifications) { - lock (this.hangNotifications) - { - listeners = this.hangNotifications.ToList(); - } + listeners = this.hangNotifications.ToList(); } + } - foreach (JoinableTaskContextNode? listener in listeners) + foreach (JoinableTaskContextNode? listener in listeners) + { + try { - try - { - listener.OnFalseHangDetected(hangDuration, hangId); - } - catch (Exception ex) - { - // Report it in CHK, but don't throw. In a hang situation, we don't want the product - // to fail for another reason, thus hiding the hang issue. - Report.Fail("Exception thrown from OnHangDetected listener. {0}", ex); - } + listener.OnFalseHangDetected(hangDuration, hangId); + } + catch (Exception ex) + { + // Report it in CHK, but don't throw. In a hang situation, we don't want the product + // to fail for another reason, thus hiding the hang issue. + Report.Fail("Exception thrown from OnHangDetected listener. {0}", ex); } } + } - /// - /// Creates a factory without a . - /// - /// - /// Used for initializing the property. - /// - protected internal virtual JoinableTaskFactory CreateDefaultFactory() + /// + /// Creates a factory without a . + /// + /// + /// Used for initializing the property. + /// + protected internal virtual JoinableTaskFactory CreateDefaultFactory() + { + return new JoinableTaskFactory(this); + } + + /// + /// Disposes managed and unmanaged resources held by this instance. + /// + /// if was called; if the object is being finalized. + protected virtual void Dispose(bool disposing) + { + } + + /// + /// Searches a parent token for a task ID that belongs to this instance. + /// + /// A parent token. + /// The 0-based index into the string where the context of the local task ID begins, if found; otherwise -1. + private int FindOurTaskId(ReadOnlySpan parentToken) + { + // Fetch the unique id for the JoinableTask that came from *this* context, if any. + int matchingContextIndex = parentToken.IndexOf(this.contextId.AsSpan(), StringComparison.Ordinal); + if (matchingContextIndex < 0) { - return new JoinableTaskFactory(this); + return -1; } - /// - /// Disposes managed and unmanaged resources held by this instance. - /// - /// true if was called; false if the object is being finalized. - protected virtual void Dispose(bool disposing) + // IMPORTANT: As the parent token frequently comes in over RPC, take care to never throw exceptions based on bad input + // as we're called on a critical scheduling callstack where an exception would lead to an Environment.FailFast call. + // To that end, only report that we found the task id if the remaining string is long enough to support it. + int uniqueIdStartIndex = matchingContextIndex + this.contextId.Length + 1; + if (parentToken.Length < uniqueIdStartIndex + TaskIdHexLength) { + return -1; } + return uniqueIdStartIndex; + } + + /// + /// Gets the task ID that came from this that is carried in a given a parent token. + /// + /// A parent token. + /// The characters that formulate the task ID that originally came from this instance, if found; otherwise an empty span. + private ReadOnlySpan GetOurTaskId(ReadOnlySpan parentToken) + { + int index = this.FindOurTaskId(parentToken); + return index < 0 ? default : parentToken.Slice(index, TaskIdHexLength); + } + + /// + /// A structure that clears CallContext and SynchronizationContext async/thread statics and + /// restores those values when this structure is disposed. + /// + public readonly struct RevertRelevance : IDisposable + { + private readonly JoinableTaskContext? pump; + private readonly SpecializedSyncContext temporarySyncContext; + private readonly JoinableTask? oldJoinable; + /// - /// A structure that clears CallContext and SynchronizationContext async/thread statics and - /// restores those values when this structure is disposed. + /// Initializes a new instance of the struct. /// - public readonly struct RevertRelevance : IDisposable + /// The instance that created this value. + internal RevertRelevance(JoinableTaskContext pump) { - private readonly JoinableTaskContext? pump; - private readonly SpecializedSyncContext temporarySyncContext; - private readonly JoinableTask? oldJoinable; - - /// - /// Initializes a new instance of the struct. - /// - /// The instance that created this value. - internal RevertRelevance(JoinableTaskContext pump) - { - Requires.NotNull(pump, nameof(pump)); - this.pump = pump; + Requires.NotNull(pump, nameof(pump)); + this.pump = pump; - this.oldJoinable = pump.AmbientTask; - pump.AmbientTask = null; - - if (SynchronizationContext.Current is JoinableTaskSynchronizationContext jobSyncContext) - { - SynchronizationContext? appliedSyncContext = null; - if (jobSyncContext.MainThreadAffinitized) - { - appliedSyncContext = pump.UnderlyingSynchronizationContext; - } + this.oldJoinable = pump.AmbientTask; + pump.AmbientTask = null; - this.temporarySyncContext = appliedSyncContext.Apply(); // Apply() extension method allows null receiver - } - else - { - this.temporarySyncContext = default(SpecializedSyncContext); - } - } - - /// - /// Reverts the async local and thread static values to their original values. - /// - public void Dispose() + if (SynchronizationContext.Current is JoinableTaskSynchronizationContext jobSyncContext) { - if (this.pump is object) + SynchronizationContext? appliedSyncContext = null; + if (jobSyncContext.MainThreadAffinitized) { - this.pump.AmbientTask = this.oldJoinable; + appliedSyncContext = pump.UnderlyingSynchronizationContext; } - this.temporarySyncContext.Dispose(); + this.temporarySyncContext = appliedSyncContext.Apply(); // Apply() extension method allows null receiver + } + else + { + this.temporarySyncContext = default(SpecializedSyncContext); } } /// - /// A class to encapsulate the details of a possible hang. - /// An instance of this class will be passed to the - /// instances who registered the hang notifications. + /// Reverts the async local and thread static values to their original values. /// - public class HangDetails + public void Dispose() { - /// Initializes a new instance of the class. - /// The duration of the current hang. - /// The number of times this hang has been reported, including this one. - /// A random GUID that uniquely identifies this particular hang. - /// The method that served as the entrypoint for the JoinableTask. - public HangDetails(TimeSpan hangDuration, int notificationCount, Guid hangId, MethodInfo? entryMethod) + if (this.pump is object) { - this.HangDuration = hangDuration; - this.NotificationCount = notificationCount; - this.HangId = hangId; - this.EntryMethod = entryMethod; + this.pump.AmbientTask = this.oldJoinable; } - /// - /// Gets the length of time this hang has lasted so far. - /// - public TimeSpan HangDuration { get; private set; } - - /// - /// Gets the number of times this particular hang has been reported, including this one. - /// - public int NotificationCount { get; private set; } - - /// - /// Gets a unique GUID identifying this particular hang. - /// If the same hang is reported multiple times (with increasing duration values) - /// the value of this property will remain constant. - /// - public Guid HangId { get; private set; } - - /// - /// Gets the method that served as the entrypoint for the JoinableTask that now blocks a thread. - /// - /// - /// The method indicated here may not be the one that is actually blocking a thread, - /// but typically a deadlock is caused by a violation of a threading rule which is under - /// the entrypoint's control. So usually regardless of where someone chooses the block - /// a thread for the completion of a , a hang usually indicates - /// a bug in the code that created it. - /// This value may be used to assign the hangs to different buckets based on this method info. - /// - public MethodInfo? EntryMethod { get; private set; } + this.temporarySyncContext.Dispose(); + } + } + + /// + /// A class to encapsulate the details of a possible hang. + /// An instance of this class will be passed to the + /// instances who registered the hang notifications. + /// + public class HangDetails + { + /// Initializes a new instance of the class. + /// The duration of the current hang. + /// The number of times this hang has been reported, including this one. + /// A random GUID that uniquely identifies this particular hang. + /// The method that served as the entrypoint for the JoinableTask. + public HangDetails(TimeSpan hangDuration, int notificationCount, Guid hangId, MethodInfo? entryMethod) + { + this.HangDuration = hangDuration; + this.NotificationCount = notificationCount; + this.HangId = hangId; + this.EntryMethod = entryMethod; } /// - /// A value whose disposal cancels hang registration. + /// Gets the length of time this hang has lasted so far. /// - private class HangNotificationRegistration : IDisposable + public TimeSpan HangDuration { get; private set; } + + /// + /// Gets the number of times this particular hang has been reported, including this one. + /// + public int NotificationCount { get; private set; } + + /// + /// Gets a unique GUID identifying this particular hang. + /// If the same hang is reported multiple times (with increasing duration values) + /// the value of this property will remain constant. + /// + public Guid HangId { get; private set; } + + /// + /// Gets the method that served as the entrypoint for the JoinableTask that now blocks a thread. + /// + /// + /// The method indicated here may not be the one that is actually blocking a thread, + /// but typically a deadlock is caused by a violation of a threading rule which is under + /// the entrypoint's control. So usually regardless of where someone chooses the block + /// a thread for the completion of a , a hang usually indicates + /// a bug in the code that created it. + /// This value may be used to assign the hangs to different buckets based on this method info. + /// + public MethodInfo? EntryMethod { get; private set; } + } + + /// + /// A value whose disposal cancels hang registration. + /// + private class HangNotificationRegistration : IDisposable + { + /// + /// The node to receive notifications. May be if has already been called. + /// + private JoinableTaskContextNode? node; + + /// + /// Initializes a new instance of the class. + /// + internal HangNotificationRegistration(JoinableTaskContextNode node) { - /// - /// The node to receive notifications. May be null if has already been called. - /// - private JoinableTaskContextNode? node; - - /// - /// Initializes a new instance of the class. - /// - internal HangNotificationRegistration(JoinableTaskContextNode node) - { - Requires.NotNull(node, nameof(node)); - this.node = node; - } + Requires.NotNull(node, nameof(node)); + this.node = node; + } - /// - /// Removes the node from hang notifications. - /// - public void Dispose() + /// + /// Removes the node from hang notifications. + /// + public void Dispose() + { + JoinableTaskContextNode? node = this.node; + if (node is object) { - JoinableTaskContextNode? node = this.node; - if (node is object) + using (node.Context.NoMessagePumpSynchronizationContext.Apply()) { - using (node.Context.NoMessagePumpSynchronizationContext.Apply()) + lock (node.Context.hangNotifications) { - lock (node.Context.hangNotifications) - { - Assumes.True(node.Context.hangNotifications.Remove(node)); - } + Assumes.True(node.Context.hangNotifications.Remove(node)); } - - this.node = null; } + + this.node = null; } } } diff --git a/src/Microsoft.VisualStudio.Threading/JoinableTaskContextException.cs b/src/Microsoft.VisualStudio.Threading/JoinableTaskContextException.cs index fa0b58010..03ebfd82a 100644 --- a/src/Microsoft.VisualStudio.Threading/JoinableTaskContextException.cs +++ b/src/Microsoft.VisualStudio.Threading/JoinableTaskContextException.cs @@ -1,52 +1,54 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading -{ - using System; +using System; + +namespace Microsoft.VisualStudio.Threading; +/// +/// An exception thrown when the configuration provided to the +/// are incorrect or a virtual method is overridden such that it violates a contract. +/// This exception should not be caught. It is thrown when the application has a programming fault. +/// +[Serializable] +public class JoinableTaskContextException : Exception +{ /// - /// An exception thrown when the configuration provided to the - /// are incorrect or a virtual method is overridden such that it violates a contract. - /// This exception should not be caught. It is thrown when the application has a programming fault. + /// Initializes a new instance of the class. /// - [Serializable] - public class JoinableTaskContextException : Exception + public JoinableTaskContextException() { - /// - /// Initializes a new instance of the class. - /// - public JoinableTaskContextException() - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The message for the exception. - public JoinableTaskContextException(string? message) - : base(message) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The message for the exception. + public JoinableTaskContextException(string? message) + : base(message) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The message for the exception. - /// The inner exception. - public JoinableTaskContextException(string? message, Exception? inner) - : base(message, inner) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The message for the exception. + /// The inner exception. + public JoinableTaskContextException(string? message, Exception? inner) + : base(message, inner) + { + } - /// - /// Initializes a new instance of the class. - /// - protected JoinableTaskContextException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) - : base(info, context) - { - } + /// + /// Initializes a new instance of the class. + /// +#if NET + [Obsolete] +#endif + protected JoinableTaskContextException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) + : base(info, context) + { } } diff --git a/src/Microsoft.VisualStudio.Threading/JoinableTaskContextNode.cs b/src/Microsoft.VisualStudio.Threading/JoinableTaskContextNode.cs index aefd29c65..9c96f73c4 100644 --- a/src/Microsoft.VisualStudio.Threading/JoinableTaskContextNode.cs +++ b/src/Microsoft.VisualStudio.Threading/JoinableTaskContextNode.cs @@ -1,201 +1,200 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// A customizable source of instances. +/// +public class JoinableTaskContextNode { - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Linq; - using System.Text; - using System.Threading; - using System.Threading.Tasks; + /// + /// The inner JoinableTaskContext. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly JoinableTaskContext context; /// - /// A customizable source of instances. + /// A single joinable task factory that itself cannot be joined. /// - public class JoinableTaskContextNode + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private JoinableTaskFactory? nonJoinableFactory; + + /// + /// Initializes a new instance of the class. + /// + /// The inner JoinableTaskContext. + public JoinableTaskContextNode(JoinableTaskContext context) { - /// - /// The inner JoinableTaskContext. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private readonly JoinableTaskContext context; - - /// - /// A single joinable task factory that itself cannot be joined. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private JoinableTaskFactory? nonJoinableFactory; - - /// - /// Initializes a new instance of the class. - /// - /// The inner JoinableTaskContext. - public JoinableTaskContextNode(JoinableTaskContext context) - { - Requires.NotNull(context, nameof(context)); - this.context = context; - } + Requires.NotNull(context, nameof(context)); + this.context = context; + } - /// - /// Gets the factory which creates joinable tasks - /// that do not belong to a joinable task collection. - /// - public JoinableTaskFactory Factory + /// + /// Gets the factory which creates joinable tasks + /// that do not belong to a joinable task collection. + /// + public JoinableTaskFactory Factory + { + get { - get + if (this.nonJoinableFactory is null) { - if (this.nonJoinableFactory is null) - { - JoinableTaskFactory? factory = this.CreateDefaultFactory(); - Interlocked.CompareExchange(ref this.nonJoinableFactory, factory, null); - } - - return this.nonJoinableFactory; + JoinableTaskFactory? factory = this.CreateDefaultFactory(); + Interlocked.CompareExchange(ref this.nonJoinableFactory, factory, null); } - } - /// - /// Gets the main thread that can be shared by tasks created by this context. - /// - public Thread MainThread - { - get { return this.context.MainThread; } + return this.nonJoinableFactory; } + } - /// - /// Gets a value indicating whether the caller is executing on the main thread. - /// - public bool IsOnMainThread => this.context.IsOnMainThread; + /// + /// Gets the main thread that can be shared by tasks created by this context. + /// + public Thread MainThread + { + get { return this.context.MainThread; } + } - /// - /// Gets the inner wrapped context. - /// - public JoinableTaskContext Context - { - get { return this.context; } - } + /// + /// Gets a value indicating whether the caller is executing on the main thread. + /// + public bool IsOnMainThread => this.context.IsOnMainThread; - /// - /// Creates a joinable task factory that automatically adds all created tasks - /// to a collection that can be jointly joined. - /// - /// The collection that all tasks should be added to. - public virtual JoinableTaskFactory CreateFactory(JoinableTaskCollection collection) - { - return this.context.CreateFactory(collection); - } + /// + /// Gets the inner wrapped context. + /// + public JoinableTaskContext Context + { + get { return this.context; } + } - /// - /// Creates a collection for in-flight joinable tasks. - /// - /// A new joinable task collection. - public JoinableTaskCollection CreateCollection() - { - return this.context.CreateCollection(); - } + /// + /// Creates a joinable task factory that automatically adds all created tasks + /// to a collection that can be jointly joined. + /// + /// The collection that all tasks should be added to. + public virtual JoinableTaskFactory CreateFactory(JoinableTaskCollection collection) + { + return this.context.CreateFactory(collection); + } - /// - /// Conceals any JoinableTask the caller is associated with until the returned value is disposed. - /// - /// A value to dispose of to restore visibility into the caller's associated JoinableTask, if any. - /// - /// In some cases asynchronous work may be spun off inside a delegate supplied to Run, - /// so that the work does not have privileges to re-enter the Main thread until the - /// call has returned and the UI thread is idle. - /// To prevent the asynchronous work from automatically being allowed to re-enter the Main thread, - /// wrap the code that calls the asynchronous task in a using block with a call to this method - /// as the expression. - /// - /// - /// this.JoinableTaskContext.RunSynchronously(async delegate { - /// using(this.JoinableTaskContext.SuppressRelevance()) { - /// var asyncOperation = Task.Run(async delegate { - /// // Some background work. - /// await this.JoinableTaskContext.SwitchToMainThreadAsync(); - /// // Some Main thread work, that cannot begin until the outer RunSynchronously call has returned. - /// }); - /// } - /// - /// // Because the asyncOperation is not related to this Main thread work (it was suppressed), - /// // the following await *would* deadlock if it were uncommented. - /// ////await asyncOperation; - /// }); - /// - /// - /// - public JoinableTaskContext.RevertRelevance SuppressRelevance() - { - return this.context.SuppressRelevance(); - } + /// + /// Creates a collection for in-flight joinable tasks. + /// + /// A new joinable task collection. + public JoinableTaskCollection CreateCollection() + { + return this.context.CreateCollection(); + } - /// - /// Gets a value indicating whether the main thread is blocked for the caller's completion. - /// - public bool IsMainThreadBlocked() - { - return this.context.IsMainThreadBlocked(); - } + /// + /// Conceals any JoinableTask the caller is associated with until the returned value is disposed. + /// + /// A value to dispose of to restore visibility into the caller's associated JoinableTask, if any. + /// + /// In some cases asynchronous work may be spun off inside a delegate supplied to Run, + /// so that the work does not have privileges to re-enter the Main thread until the + /// call has returned and the UI thread is idle. + /// To prevent the asynchronous work from automatically being allowed to re-enter the Main thread, + /// wrap the code that calls the asynchronous task in a using block with a call to this method + /// as the expression. + /// + /// + /// this.JoinableTaskContext.RunSynchronously(async delegate { + /// using(this.JoinableTaskContext.SuppressRelevance()) { + /// var asyncOperation = Task.Run(async delegate { + /// // Some background work. + /// await this.JoinableTaskContext.SwitchToMainThreadAsync(); + /// // Some Main thread work, that cannot begin until the outer RunSynchronously call has returned. + /// }); + /// } + /// + /// // Because the asyncOperation is not related to this Main thread work (it was suppressed), + /// // the following await *would* deadlock if it were uncommented. + /// ////await asyncOperation; + /// }); + /// + /// + /// + public JoinableTaskContext.RevertRelevance SuppressRelevance() + { + return this.context.SuppressRelevance(); + } - /// - /// Invoked when a hang is suspected to have occurred involving the main thread. - /// - /// Describes the hang in detail. - /// - /// A single hang occurrence may invoke this method multiple times, with increasing - /// values in the values - /// in the parameter. - /// - protected internal virtual void OnHangDetected(JoinableTaskContext.HangDetails details) - { - Requires.NotNull(details, nameof(details)); + /// + /// Gets a value indicating whether the main thread is blocked for the caller's completion. + /// + public bool IsMainThreadBlocked() + { + return this.context.IsMainThreadBlocked(); + } - // Preserve backward compatibility by forwarding the call to the older overload. - this.OnHangDetected(details.HangDuration, details.NotificationCount, details.HangId); - } + /// + /// Invoked when a hang is suspected to have occurred involving the main thread. + /// + /// Describes the hang in detail. + /// + /// A single hang occurrence may invoke this method multiple times, with increasing + /// values in the values + /// in the parameter. + /// + protected internal virtual void OnHangDetected(JoinableTaskContext.HangDetails details) + { + Requires.NotNull(details, nameof(details)); - /// - /// Invoked when an earlier hang report is false alarm. - /// - /// The duration of the total waiting time. - /// A GUID that uniquely identifies the earlier hang report. - protected internal virtual void OnFalseHangDetected(TimeSpan hangDuration, Guid hangId) - { - } + // Preserve backward compatibility by forwarding the call to the older overload. + this.OnHangDetected(details.HangDuration, details.NotificationCount, details.HangId); + } - /// - /// Invoked when a hang is suspected to have occurred involving the main thread. - /// - /// The duration of the current hang. - /// The number of times this hang has been reported, including this one. - /// A random GUID that uniquely identifies this particular hang. - /// - /// A single hang occurrence may invoke this method multiple times, with increasing - /// values in the parameter. - /// - protected virtual void OnHangDetected(TimeSpan hangDuration, int notificationCount, Guid hangId) - { - } + /// + /// Invoked when an earlier hang report is false alarm. + /// + /// The duration of the total waiting time. + /// A GUID that uniquely identifies the earlier hang report. + protected internal virtual void OnFalseHangDetected(TimeSpan hangDuration, Guid hangId) + { + } - /// - /// Creates a factory without a . - /// - /// - /// Used for initializing the property. - /// - protected virtual JoinableTaskFactory CreateDefaultFactory() - { - return this.context.CreateDefaultFactory(); - } + /// + /// Invoked when a hang is suspected to have occurred involving the main thread. + /// + /// The duration of the current hang. + /// The number of times this hang has been reported, including this one. + /// A random GUID that uniquely identifies this particular hang. + /// + /// A single hang occurrence may invoke this method multiple times, with increasing + /// values in the parameter. + /// + protected virtual void OnHangDetected(TimeSpan hangDuration, int notificationCount, Guid hangId) + { + } - /// - /// Registers with the inner to receive hang notifications. - /// - /// A value to dispose of to cancel hang notifications. - protected IDisposable RegisterOnHangDetected() - { - return this.context.RegisterHangNotifications(this); - } + /// + /// Creates a factory without a . + /// + /// + /// Used for initializing the property. + /// + protected virtual JoinableTaskFactory CreateDefaultFactory() + { + return this.context.CreateDefaultFactory(); + } + + /// + /// Registers with the inner to receive hang notifications. + /// + /// A value to dispose of to cancel hang notifications. + protected IDisposable RegisterOnHangDetected() + { + return this.context.RegisterHangNotifications(this); } } diff --git a/src/Microsoft.VisualStudio.Threading/JoinableTaskCreationOptions.cs b/src/Microsoft.VisualStudio.Threading/JoinableTaskCreationOptions.cs index 39fb0d4fd..b2e81852e 100644 --- a/src/Microsoft.VisualStudio.Threading/JoinableTaskCreationOptions.cs +++ b/src/Microsoft.VisualStudio.Threading/JoinableTaskCreationOptions.cs @@ -1,26 +1,25 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading -{ - using System; +using System; + +namespace Microsoft.VisualStudio.Threading; +/// +/// Specifies flags that control optional behavior for the creation and execution of tasks. +/// +[Flags] +[Serializable] +public enum JoinableTaskCreationOptions +{ /// - /// Specifies flags that control optional behavior for the creation and execution of tasks. + /// Specifies that the default behavior should be used. /// - [Flags] - [Serializable] - public enum JoinableTaskCreationOptions - { - /// - /// Specifies that the default behavior should be used. - /// - None = 0x0, + None = 0x0, - /// - /// Specifies that a task will be a long-running operation. It provides a hint to the - /// that hang report should not be fired, when the main thread task is blocked on it. - /// - LongRunning = 0x01, - } + /// + /// Specifies that a task will be a long-running operation. It provides a hint to the + /// that hang report should not be fired, when the main thread task is blocked on it. + /// + LongRunning = 0x01, } diff --git a/src/Microsoft.VisualStudio.Threading/JoinableTaskDependencyGraph.cs b/src/Microsoft.VisualStudio.Threading/JoinableTaskDependencyGraph.cs index 1ae6b256a..c0de4ac3f 100644 --- a/src/Microsoft.VisualStudio.Threading/JoinableTaskDependencyGraph.cs +++ b/src/Microsoft.VisualStudio.Threading/JoinableTaskDependencyGraph.cs @@ -1,1116 +1,1114 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// Methods to maintain dependencies between . +/// Those methods are expected to be called by or only to maintain relationship between them, and should not be called directly by other code. +/// +internal static class JoinableTaskDependencyGraph { - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Diagnostics.CodeAnalysis; - using System.Linq; - using System.Threading; + private static readonly HashSet EmptySet = new HashSet(); /// - /// Methods to maintain dependencies between . - /// Those methods are expected to be called by or only to maintain relationship between them, and should not be called directly by other code. + /// Gets a value indicating whether there is no child depenent item. + /// This method is expected to be used with the JTF lock. /// - internal static class JoinableTaskDependencyGraph + internal static bool HasNoChildDependentNode(IJoinableTaskDependent taskItem) { - private static readonly HashSet EmptySet = new HashSet(); - - /// - /// Gets a value indicating whether there is no child depenent item. - /// This method is expected to be used with the JTF lock. - /// - internal static bool HasNoChildDependentNode(IJoinableTaskDependent taskItem) - { - Requires.NotNull(taskItem, nameof(taskItem)); - Assumes.True(Monitor.IsEntered(taskItem.JoinableTaskContext.SyncContextLock)); - return taskItem.GetJoinableTaskDependentData().HasNoChildDependentNode; - } + Requires.NotNull(taskItem, nameof(taskItem)); + Assumes.True(Monitor.IsEntered(taskItem.JoinableTaskContext.SyncContextLock)); + return taskItem.GetJoinableTaskDependentData().HasNoChildDependentNode; + } - /// - /// Checks whether a task or collection is a directly dependent of this item. - /// This method is expected to be used with the JTF lock. - /// - internal static bool HasDirectDependency(IJoinableTaskDependent taskItem, IJoinableTaskDependent dependency) - { - Requires.NotNull(taskItem, nameof(taskItem)); - Assumes.True(Monitor.IsEntered(taskItem.JoinableTaskContext.SyncContextLock)); - return taskItem.GetJoinableTaskDependentData().HasDirectDependency(dependency); - } + /// + /// Checks whether a task or collection is a directly dependent of this item. + /// This method is expected to be used with the JTF lock. + /// + internal static bool HasDirectDependency(IJoinableTaskDependent taskItem, IJoinableTaskDependent dependency) + { + Requires.NotNull(taskItem, nameof(taskItem)); + Assumes.True(Monitor.IsEntered(taskItem.JoinableTaskContext.SyncContextLock)); + return taskItem.GetJoinableTaskDependentData().HasDirectDependency(dependency); + } - /// - /// Gets a value indicating whether the main thread is waiting for the task's completion. - /// - internal static bool HasMainThreadSynchronousTaskWaiting(IJoinableTaskDependent taskItem) + /// + /// Gets a value indicating whether the main thread is waiting for the task's completion. + /// + internal static bool HasMainThreadSynchronousTaskWaiting(IJoinableTaskDependent taskItem) + { + Requires.NotNull(taskItem, nameof(taskItem)); + using (taskItem.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) { - Requires.NotNull(taskItem, nameof(taskItem)); - using (taskItem.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) + lock (taskItem.JoinableTaskContext.SyncContextLock) { - lock (taskItem.JoinableTaskContext.SyncContextLock) - { - return taskItem.GetJoinableTaskDependentData().HasMainThreadSynchronousTaskWaiting(taskItem); - } + return taskItem.GetJoinableTaskDependentData().HasMainThreadSynchronousTaskWaiting(taskItem); } } + } - /// - /// Gets a likely value whether the main thread is blocked for the caller's completion. - /// - internal static bool MaybeHasMainThreadSynchronousTaskWaiting(IJoinableTaskDependent taskItem) - { - return taskItem.GetJoinableTaskDependentData().MaybeHasMainThreadSynchronousTaskWaiting(); - } + /// + /// Gets a likely value whether the main thread is blocked for the caller's completion. + /// + internal static bool MaybeHasMainThreadSynchronousTaskWaiting(IJoinableTaskDependent taskItem) + { + return taskItem.GetJoinableTaskDependentData().MaybeHasMainThreadSynchronousTaskWaiting(); + } - /// - /// Adds a instance as one that is relevant to the async operation. - /// - /// The current joinableTask or collection. - /// The to join as a child. - internal static JoinableTaskCollection.JoinRelease AddDependency(IJoinableTaskDependent taskItem, IJoinableTaskDependent joinChild) - { - Requires.NotNull(taskItem, nameof(taskItem)); - return JoinableTaskDependentData.AddDependency(taskItem, joinChild); - } + /// + /// Adds a instance as one that is relevant to the async operation. + /// + /// The current joinableTask or collection. + /// The to join as a child. + internal static JoinableTaskCollection.JoinRelease AddDependency(IJoinableTaskDependent taskItem, IJoinableTaskDependent joinChild) + { + Requires.NotNull(taskItem, nameof(taskItem)); + return JoinableTaskDependentData.AddDependency(taskItem, joinChild); + } - /// - /// Removes a instance as one that is no longer relevant to the async operation. - /// - /// The current joinableTask or collection. - /// The to join as a child. - /// Ignore refCount, it is being used when the child task is completed. - internal static void RemoveDependency(IJoinableTaskDependent taskItem, IJoinableTaskDependent child, bool forceCleanup = false) - { - Requires.NotNull(taskItem, nameof(taskItem)); - JoinableTaskDependentData.RemoveDependency(taskItem, child, forceCleanup); - } + /// + /// Removes a instance as one that is no longer relevant to the async operation. + /// + /// The current joinableTask or collection. + /// The to join as a child. + /// Ignore refCount, it is being used when the child task is completed. + internal static void RemoveDependency(IJoinableTaskDependent taskItem, IJoinableTaskDependent child, bool forceCleanup = false) + { + Requires.NotNull(taskItem, nameof(taskItem)); + JoinableTaskDependentData.RemoveDependency(taskItem, child, forceCleanup); + } - /// - /// Gets all dependent nodes registered in the dependency collection. - /// This method is expected to be used with the JTF lock. - /// - internal static IEnumerable GetDirectDependentNodes(IJoinableTaskDependent taskItem) - { - Requires.NotNull(taskItem, nameof(taskItem)); - Assumes.True(Monitor.IsEntered(taskItem.JoinableTaskContext.SyncContextLock)); - return taskItem.GetJoinableTaskDependentData().GetDirectDependentNodes(); - } + /// + /// Gets all dependent nodes registered in the dependency collection. + /// This method is expected to be used with the JTF lock. + /// + internal static IEnumerable GetDirectDependentNodes(IJoinableTaskDependent taskItem) + { + Requires.NotNull(taskItem, nameof(taskItem)); + Assumes.True(Monitor.IsEntered(taskItem.JoinableTaskContext.SyncContextLock)); + return taskItem.GetJoinableTaskDependentData().GetDirectDependentNodes(); + } - /// - /// Check whether a task is being tracked in our tracking list. - /// - internal static bool IsDependingSynchronousTask(IJoinableTaskDependent taskItem, JoinableTask syncTask) - { - Requires.NotNull(taskItem, nameof(taskItem)); - return taskItem.GetJoinableTaskDependentData().IsDependingSynchronousTask(syncTask); - } + /// + /// Check whether a task is being tracked in our tracking list. + /// + internal static bool IsDependingSynchronousTask(IJoinableTaskDependent taskItem, JoinableTask syncTask) + { + Requires.NotNull(taskItem, nameof(taskItem)); + return taskItem.GetJoinableTaskDependentData().IsDependingSynchronousTask(syncTask); + } - /// - /// Calculate the collection of events we need trigger after we enqueue a request. - /// This method is expected to be used with the JTF lock. - /// - /// The current joinableTask or collection. - /// True if we want to find tasks to process the main thread queue. Otherwise tasks to process the background queue. - /// The collection of synchronous tasks we need notify. - internal static IReadOnlyCollection GetDependingSynchronousTasks(IJoinableTaskDependent taskItem, bool forMainThread) - { - Requires.NotNull(taskItem, nameof(taskItem)); - Assumes.True(Monitor.IsEntered(taskItem.JoinableTaskContext.SyncContextLock)); - return taskItem.GetJoinableTaskDependentData().GetDependingSynchronousTasks(forMainThread); - } + /// + /// Calculate the collection of events we need trigger after we enqueue a request. + /// This method is expected to be used with the JTF lock. + /// + /// The current joinableTask or collection. + /// True if we want to find tasks to process the main thread queue. Otherwise tasks to process the background queue. + /// The collection of synchronous tasks we need notify. + internal static IReadOnlyCollection GetDependingSynchronousTasks(IJoinableTaskDependent taskItem, bool forMainThread) + { + Requires.NotNull(taskItem, nameof(taskItem)); + Assumes.True(Monitor.IsEntered(taskItem.JoinableTaskContext.SyncContextLock)); + return taskItem.GetJoinableTaskDependentData().GetDependingSynchronousTasks(forMainThread); + } - /// - /// Gets a snapshot of all joined tasks. - /// FOR DIAGNOSTICS COLLECTION ONLY. - /// This method is expected to be used with the JTF lock. - /// - internal static IEnumerable GetAllDirectlyDependentJoinableTasks(IJoinableTaskDependent taskItem) - { - Requires.NotNull(taskItem, nameof(taskItem)); - return JoinableTaskDependentData.GetAllDirectlyDependentJoinableTasks(taskItem); - } + /// + /// Gets a snapshot of all joined tasks. + /// FOR DIAGNOSTICS COLLECTION ONLY. + /// This method is expected to be used with the JTF lock. + /// + internal static IEnumerable GetAllDirectlyDependentJoinableTasks(IJoinableTaskDependent taskItem) + { + Requires.NotNull(taskItem, nameof(taskItem)); + return JoinableTaskDependentData.GetAllDirectlyDependentJoinableTasks(taskItem); + } - /// - /// Recursively adds this joinable and all its dependencies to the specified set, that are not yet completed. - /// - internal static void AddSelfAndDescendentOrJoinedJobs(IJoinableTaskDependent taskItem, HashSet joinables) - { - Requires.NotNull(taskItem, nameof(taskItem)); - JoinableTaskDependentData.AddSelfAndDescendentOrJoinedJobs(taskItem, joinables); - } + /// + /// Recursively adds this joinable and all its dependencies to the specified set, that are not yet completed. + /// + internal static void AddSelfAndDescendentOrJoinedJobs(IJoinableTaskDependent taskItem, HashSet joinables) + { + Requires.NotNull(taskItem, nameof(taskItem)); + JoinableTaskDependentData.AddSelfAndDescendentOrJoinedJobs(taskItem, joinables); + } - /// - /// When the current dependent node is a synchronous task, this method is called before the thread is blocked to wait it to complete. - /// This adds the current task to the dependingSynchronousTaskTracking list of the task itself (which will propergate through its dependencies.) - /// After the task is finished, is called to revert this change. - /// This method is expected to be used with the JTF lock. - /// - /// The current joinableTask or collection. - /// Return the JoinableTask which has already had pending requests to be handled. - /// The number of pending requests. - internal static void OnSynchronousTaskStartToBlockWaiting(JoinableTask taskItem, out JoinableTask? taskHasPendingRequests, out int pendingRequestsCount) - { - Requires.NotNull(taskItem, nameof(taskItem)); - Assumes.True(Monitor.IsEntered(taskItem.Factory.Context.SyncContextLock)); - JoinableTaskDependentData.OnSynchronousTaskStartToBlockWaiting(taskItem, out taskHasPendingRequests, out pendingRequestsCount); - } + /// + /// When the current dependent node is a synchronous task, this method is called before the thread is blocked to wait it to complete. + /// This adds the current task to the dependingSynchronousTaskTracking list of the task itself (which will propergate through its dependencies.) + /// After the task is finished, is called to revert this change. + /// This method is expected to be used with the JTF lock. + /// + /// The current joinableTask or collection. + /// Return the JoinableTask which has already had pending requests to be handled. + /// The number of pending requests. + internal static void OnSynchronousTaskStartToBlockWaiting(JoinableTask taskItem, out JoinableTask? taskHasPendingRequests, out int pendingRequestsCount) + { + Requires.NotNull(taskItem, nameof(taskItem)); + Assumes.True(Monitor.IsEntered(taskItem.Factory.Context.SyncContextLock)); + JoinableTaskDependentData.OnSynchronousTaskStartToBlockWaiting(taskItem, out taskHasPendingRequests, out pendingRequestsCount); + } - /// - /// When the current dependent node is a synchronous task, this method is called after the synchronous is completed, and the thread is no longer blocked. - /// This removes the current task from the dependingSynchronousTaskTracking list of the task itself (and propergate through its dependencies.) - /// It reverts the data structure change done in the . - /// - internal static void OnSynchronousTaskEndToBlockWaiting(JoinableTask taskItem) - { - Requires.NotNull(taskItem, nameof(taskItem)); - JoinableTaskDependentData.OnSynchronousTaskEndToBlockWaiting(taskItem); - } + /// + /// When the current dependent node is a synchronous task, this method is called after the synchronous is completed, and the thread is no longer blocked. + /// This removes the current task from the dependingSynchronousTaskTracking list of the task itself (and propergate through its dependencies.) + /// It reverts the data structure change done in the . + /// + internal static void OnSynchronousTaskEndToBlockWaiting(JoinableTask taskItem) + { + Requires.NotNull(taskItem, nameof(taskItem)); + JoinableTaskDependentData.OnSynchronousTaskEndToBlockWaiting(taskItem); + } - /// - /// Remove all synchronous tasks tracked by the this task. - /// This is called when this task is completed. - /// This method is expected to be used with the JTF lock. - /// - internal static void OnTaskCompleted(IJoinableTaskDependent taskItem) + /// + /// Remove all synchronous tasks tracked by the this task. + /// This is called when this task is completed. + /// This method is expected to be used with the JTF lock. + /// + internal static void OnTaskCompleted(IJoinableTaskDependent taskItem) + { + Requires.NotNull(taskItem, nameof(taskItem)); + Assumes.True(Monitor.IsEntered(taskItem.JoinableTaskContext.SyncContextLock)); + taskItem.GetJoinableTaskDependentData().OnTaskCompleted(taskItem); + } + + /// + /// Get all tasks inside the candidate sets tasks, which are depended by one or more task in the source tasks list. + /// + /// A collection of JoinableTasks represents source tasks. + /// A collection of JoinableTasks which represents candidates. + /// A set of tasks matching the condition. + internal static HashSet GetDependentTasksFromCandidates(IEnumerable sourceTasks, IEnumerable candidateTasks) + { + Requires.NotNull(sourceTasks, nameof(sourceTasks)); + Requires.NotNull(candidateTasks, nameof(candidateTasks)); + + var candidates = new HashSet(candidateTasks); + if (candidates.Count == 0) { - Requires.NotNull(taskItem, nameof(taskItem)); - Assumes.True(Monitor.IsEntered(taskItem.JoinableTaskContext.SyncContextLock)); - taskItem.GetJoinableTaskDependentData().OnTaskCompleted(taskItem); + return candidates; } - /// - /// Get all tasks inside the candidate sets tasks, which are depended by one or more task in the source tasks list. - /// - /// A collection of JoinableTasks represents source tasks. - /// A collection of JoinableTasks which represents candidates. - /// A set of tasks matching the condition. - internal static HashSet GetDependentTasksFromCandidates(IEnumerable sourceTasks, IEnumerable candidateTasks) - { - Requires.NotNull(sourceTasks, nameof(sourceTasks)); - Requires.NotNull(candidateTasks, nameof(candidateTasks)); + var results = new HashSet(); + var visited = new HashSet(); - var candidates = new HashSet(candidateTasks); - if (candidates.Count == 0) + var queue = new Queue(); + foreach (JoinableTask task in sourceTasks) + { + if (task is not null && visited.Add(task)) { - return candidates; + queue.Enqueue(task); } + } - var results = new HashSet(); - var visited = new HashSet(); - - var queue = new Queue(); - foreach (JoinableTask task in sourceTasks) + while (queue.Count > 0) + { + IJoinableTaskDependent startDepenentNode = queue.Dequeue(); + if (startDepenentNode is JoinableTask startTask && candidates.Contains(startTask)) { - if (task is not null && visited.Add(task)) - { - queue.Enqueue(task); - } + results.Add(startTask); } - while (queue.Count > 0) + lock (startDepenentNode.JoinableTaskContext.SyncContextLock) { - IJoinableTaskDependent startDepenentNode = queue.Dequeue(); - if (startDepenentNode is JoinableTask startTask && candidates.Contains(startTask)) + foreach (IJoinableTaskDependent? dependentItem in JoinableTaskDependencyGraph.GetDirectDependentNodes(startDepenentNode)) { - results.Add(startTask); - } - - lock (startDepenentNode.JoinableTaskContext.SyncContextLock) - { - foreach (IJoinableTaskDependent? dependentItem in JoinableTaskDependencyGraph.GetDirectDependentNodes(startDepenentNode)) + if (visited.Add(dependentItem)) { - if (visited.Add(dependentItem)) - { - queue.Enqueue(dependentItem); - } + queue.Enqueue(dependentItem); } } } - - return results; } - /// - /// Computes dependency graph to clean up all potential unreachable dependents items. - /// - /// A thread blocking sychornizing task. - /// Returns all reachable nodes in the connected dependency graph, if unreachable dependency is found. - /// True if it removes any unreachable items. - internal static bool CleanUpPotentialUnreachableDependentItems(JoinableTask syncTask, [NotNullWhen(true)] out HashSet? allReachableNodes) + return results; + } + + /// + /// Computes dependency graph to clean up all potential unreachable dependents items. + /// + /// A thread blocking sychornizing task. + /// Returns all reachable nodes in the connected dependency graph, if unreachable dependency is found. + /// True if it removes any unreachable items. + internal static bool CleanUpPotentialUnreachableDependentItems(JoinableTask syncTask, [NotNullWhen(true)] out HashSet? allReachableNodes) + { + Requires.NotNull(syncTask, nameof(syncTask)); + + // a set of tasks may form a dependent loop, so it will make the reference count system + // not to work correctly when we try to remove the synchronous task. + // To get rid of those loops, if a task still tracks the synchronous task after reducing + // the reference count, we will calculate the entire reachable tree from the root. That will + // tell us the exactly tasks which need track the synchronous task, and we will clean up the rest. + HashSet? possibleUnreachableItems = syncTask.PotentialUnreachableDependents; + if (possibleUnreachableItems is object && possibleUnreachableItems.Count > 0) { - Requires.NotNull(syncTask, nameof(syncTask)); + var reachableNodes = new HashSet(); + IJoinableTaskDependent syncTaskItem = syncTask; + + JoinableTaskDependentData.ComputeSelfAndDescendentOrJoinedJobsAndRemainTasks(syncTaskItem, reachableNodes, possibleUnreachableItems); - // a set of tasks may form a dependent loop, so it will make the reference count system - // not to work correctly when we try to remove the synchronous task. - // To get rid of those loops, if a task still tracks the synchronous task after reducing - // the reference count, we will calculate the entire reachable tree from the root. That will - // tell us the exactly tasks which need track the synchronous task, and we will clean up the rest. - HashSet? possibleUnreachableItems = syncTask.PotentialUnreachableDependents; - if (possibleUnreachableItems is object && possibleUnreachableItems.Count > 0) + allReachableNodes = reachableNodes; + + // force to remove all invalid items + if (possibleUnreachableItems.Count > 0) { - var reachableNodes = new HashSet(); - IJoinableTaskDependent syncTaskItem = syncTask; + JoinableTaskDependentData.RemoveUnreachableDependentItems(syncTask, possibleUnreachableItems, reachableNodes); + possibleUnreachableItems.Clear(); - JoinableTaskDependentData.ComputeSelfAndDescendentOrJoinedJobsAndRemainTasks(syncTaskItem, reachableNodes, possibleUnreachableItems); + return true; + } + } - allReachableNodes = reachableNodes; + allReachableNodes = null; + return false; + } - // force to remove all invalid items - if (possibleUnreachableItems.Count > 0) - { - JoinableTaskDependentData.RemoveUnreachableDependentItems(syncTask, possibleUnreachableItems, reachableNodes); - possibleUnreachableItems.Clear(); + /// + /// Force to clean up all unreachable dependent item, so they are not marked to block the syncTask. + /// + /// The thread blocking task. + /// Unreachable dependent items. + /// All reachable items. + internal static void RemoveUnreachableDependentItems(JoinableTask syncTask, HashSet unreachableItems, HashSet reachableItems) + { + Requires.NotNull(syncTask, nameof(syncTask)); + Requires.NotNull(unreachableItems, nameof(unreachableItems)); + Requires.NotNull(reachableItems, nameof(reachableItems)); - return true; - } - } + JoinableTaskDependentData.RemoveUnreachableDependentItems(syncTask, unreachableItems, reachableItems); + } - allReachableNodes = null; - return false; - } + /// + /// Preserve data for the JoinableTask dependency tree. It is holded inside either a or a . + /// Do not call methods/properties directly anywhere out of . + /// + internal struct JoinableTaskDependentData + { + /// + /// A map of jobs that we should be willing to dequeue from when we control the UI thread, and a ref count. Lazily constructed. + /// + /// + /// When the value in an entry is decremented to 0, the entry is removed from the map. + /// + private Dictionary childDependentNodes; /// - /// Force to clean up all unreachable dependent item, so they are not marked to block the syncTask. + /// The head of a singly linked list of records to track which task may process events of this task. + /// This list should contain only tasks which need be completed synchronously, and depends on this task. /// - /// The thread blocking task. - /// Unreachable dependent items. - /// All reachable items. - internal static void RemoveUnreachableDependentItems(JoinableTask syncTask, HashSet unreachableItems, HashSet reachableItems) - { - Requires.NotNull(syncTask, nameof(syncTask)); - Requires.NotNull(unreachableItems, nameof(unreachableItems)); - Requires.NotNull(reachableItems, nameof(reachableItems)); + private DependentSynchronousTask? dependingSynchronousTaskTracking; - JoinableTaskDependentData.RemoveUnreachableDependentItems(syncTask, unreachableItems, reachableItems); - } + /// + /// Gets a value indicating whether the is empty. + /// + internal bool HasNoChildDependentNode => this.childDependentNodes is null || this.childDependentNodes.Count == 0; /// - /// Preserve data for the JoinableTask dependency tree. It is holded inside either a or a . - /// Do not call methods/properties directly anywhere out of . + /// Gets a snapshot of all joined tasks. + /// FOR DIAGNOSTICS COLLECTION ONLY. + /// This method is expected to be used with the JTF lock. /// - internal struct JoinableTaskDependentData + /// The current joinableTask or collection contains this data. + internal static IEnumerable GetAllDirectlyDependentJoinableTasks(IJoinableTaskDependent taskOrCollection) { - /// - /// A map of jobs that we should be willing to dequeue from when we control the UI thread, and a ref count. Lazily constructed. - /// - /// - /// When the value in an entry is decremented to 0, the entry is removed from the map. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private Dictionary childDependentNodes; - - /// - /// The head of a singly linked list of records to track which task may process events of this task. - /// This list should contain only tasks which need be completed synchronously, and depends on this task. - /// - private DependentSynchronousTask? dependingSynchronousTaskTracking; + Requires.NotNull(taskOrCollection, nameof(taskOrCollection)); + Assumes.True(Monitor.IsEntered(taskOrCollection.JoinableTaskContext.SyncContextLock)); + if (taskOrCollection.GetJoinableTaskDependentData().childDependentNodes is null) + { + return Enumerable.Empty(); + } - /// - /// Gets a value indicating whether the is empty. - /// - internal bool HasNoChildDependentNode => this.childDependentNodes is null || this.childDependentNodes.Count == 0; + var allTasks = new HashSet(); + AddSelfAndDescendentOrJoinedJobs(taskOrCollection, allTasks); + return allTasks; + } - /// - /// Gets a snapshot of all joined tasks. - /// FOR DIAGNOSTICS COLLECTION ONLY. - /// This method is expected to be used with the JTF lock. - /// - /// The current joinableTask or collection contains this data. - internal static IEnumerable GetAllDirectlyDependentJoinableTasks(IJoinableTaskDependent taskOrCollection) + /// + /// Adds a instance as one that is relevant to the async operation. + /// + /// The current joinableTask or collection contains to add a dependency. + /// The to join as a child. + internal static JoinableTaskCollection.JoinRelease AddDependency(IJoinableTaskDependent parentTaskOrCollection, IJoinableTaskDependent joinChild) + { + Requires.NotNull(parentTaskOrCollection, nameof(parentTaskOrCollection)); + Requires.NotNull(joinChild, nameof(joinChild)); + if (parentTaskOrCollection == joinChild) { - Requires.NotNull(taskOrCollection, nameof(taskOrCollection)); - Assumes.True(Monitor.IsEntered(taskOrCollection.JoinableTaskContext.SyncContextLock)); - if (taskOrCollection.GetJoinableTaskDependentData().childDependentNodes is null) - { - return Enumerable.Empty(); - } - - var allTasks = new HashSet(); - AddSelfAndDescendentOrJoinedJobs(taskOrCollection, allTasks); - return allTasks; + // Joining oneself would be pointless. + return default(JoinableTaskCollection.JoinRelease); } - /// - /// Adds a instance as one that is relevant to the async operation. - /// - /// The current joinableTask or collection contains to add a dependency. - /// The to join as a child. - internal static JoinableTaskCollection.JoinRelease AddDependency(IJoinableTaskDependent parentTaskOrCollection, IJoinableTaskDependent joinChild) + using (parentTaskOrCollection.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) { - Requires.NotNull(parentTaskOrCollection, nameof(parentTaskOrCollection)); - Requires.NotNull(joinChild, nameof(joinChild)); - if (parentTaskOrCollection == joinChild) + List? eventsNeedNotify = null; + lock (parentTaskOrCollection.JoinableTaskContext.SyncContextLock) { - // Joining oneself would be pointless. - return default(JoinableTaskCollection.JoinRelease); - } - - using (parentTaskOrCollection.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) - { - List? eventsNeedNotify = null; - lock (parentTaskOrCollection.JoinableTaskContext.SyncContextLock) + var joinableTask = joinChild as JoinableTask; + if (joinableTask?.IsFullyCompleted == true) { - var joinableTask = joinChild as JoinableTask; - if (joinableTask?.IsFullyCompleted == true) - { - return default(JoinableTaskCollection.JoinRelease); - } + return default(JoinableTaskCollection.JoinRelease); + } - ref JoinableTaskDependentData data = ref parentTaskOrCollection.GetJoinableTaskDependentData(); - if (data.childDependentNodes is null) - { - data.childDependentNodes = new Dictionary(capacity: 2); - } + ref JoinableTaskDependentData data = ref parentTaskOrCollection.GetJoinableTaskDependentData(); + if (data.childDependentNodes is null) + { + data.childDependentNodes = new Dictionary(capacity: 2); + } - if (data.childDependentNodes.TryGetValue(joinChild, out int refCount) && !parentTaskOrCollection.NeedRefCountChildDependencies) - { - return default(JoinableTaskCollection.JoinRelease); - } + if (data.childDependentNodes.TryGetValue(joinChild, out int refCount) && !parentTaskOrCollection.NeedRefCountChildDependencies) + { + return default(JoinableTaskCollection.JoinRelease); + } - data.childDependentNodes[joinChild] = ++refCount; - if (refCount == 1) + data.childDependentNodes[joinChild] = ++refCount; + if (refCount == 1) + { + // This constitutes a significant change, so we should apply synchronous task tracking to the new child. + joinChild.OnAddedToDependency(parentTaskOrCollection); + IReadOnlyCollection? tasksNeedNotify = AddDependingSynchronousTaskToChild(parentTaskOrCollection, joinChild); + if (tasksNeedNotify.Count > 0) { - // This constitutes a significant change, so we should apply synchronous task tracking to the new child. - joinChild.OnAddedToDependency(parentTaskOrCollection); - IReadOnlyCollection? tasksNeedNotify = AddDependingSynchronousTaskToChild(parentTaskOrCollection, joinChild); - if (tasksNeedNotify.Count > 0) + eventsNeedNotify = new List(tasksNeedNotify.Count); + foreach (PendingNotification taskToNotify in tasksNeedNotify) { - eventsNeedNotify = new List(tasksNeedNotify.Count); - foreach (PendingNotification taskToNotify in tasksNeedNotify) + AsyncManualResetEvent? notifyEvent = taskToNotify.SynchronousTask.RegisterPendingEventsForSynchrousTask(taskToNotify.TaskHasPendingMessages, taskToNotify.NewPendingMessagesCount); + if (notifyEvent is object) { - AsyncManualResetEvent? notifyEvent = taskToNotify.SynchronousTask.RegisterPendingEventsForSynchrousTask(taskToNotify.TaskHasPendingMessages, taskToNotify.NewPendingMessagesCount); - if (notifyEvent is object) - { - eventsNeedNotify.Add(notifyEvent); - } + eventsNeedNotify.Add(notifyEvent); } } - - parentTaskOrCollection.OnDependencyAdded(joinChild); } + + parentTaskOrCollection.OnDependencyAdded(joinChild); } + } - // We explicitly do this outside our lock. - if (eventsNeedNotify is object) + // We explicitly do this outside our lock. + if (eventsNeedNotify is object) + { + foreach (AsyncManualResetEvent? queueEvent in eventsNeedNotify) { - foreach (AsyncManualResetEvent? queueEvent in eventsNeedNotify) - { - queueEvent.PulseAll(); - } + queueEvent.PulseAll(); } - - return new JoinableTaskCollection.JoinRelease(parentTaskOrCollection, joinChild); } + + return new JoinableTaskCollection.JoinRelease(parentTaskOrCollection, joinChild); } + } - /// - /// Removes a instance as one that is no longer relevant to the async operation. - /// - /// The current joinableTask or collection contains to remove a dependency. - /// The to join as a child. - /// Ignore refCount, it is being used when the child task is completed. - internal static void RemoveDependency(IJoinableTaskDependent parentTaskOrCollection, IJoinableTaskDependent joinChild, bool forceCleanup) - { - Requires.NotNull(parentTaskOrCollection, nameof(parentTaskOrCollection)); - Requires.NotNull(joinChild, nameof(joinChild)); + /// + /// Removes a instance as one that is no longer relevant to the async operation. + /// + /// The current joinableTask or collection contains to remove a dependency. + /// The to join as a child. + /// Ignore refCount, it is being used when the child task is completed. + internal static void RemoveDependency(IJoinableTaskDependent parentTaskOrCollection, IJoinableTaskDependent joinChild, bool forceCleanup) + { + Requires.NotNull(parentTaskOrCollection, nameof(parentTaskOrCollection)); + Requires.NotNull(joinChild, nameof(joinChild)); - using (parentTaskOrCollection.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) + using (parentTaskOrCollection.JoinableTaskContext.NoMessagePumpSynchronizationContext.Apply()) + { + ref JoinableTaskDependentData data = ref parentTaskOrCollection.GetJoinableTaskDependentData(); + lock (parentTaskOrCollection.JoinableTaskContext.SyncContextLock) { - ref JoinableTaskDependentData data = ref parentTaskOrCollection.GetJoinableTaskDependentData(); - lock (parentTaskOrCollection.JoinableTaskContext.SyncContextLock) + if (data.childDependentNodes is object && data.childDependentNodes.TryGetValue(joinChild, out int refCount)) { - if (data.childDependentNodes is object && data.childDependentNodes.TryGetValue(joinChild, out int refCount)) + if (refCount == 1 || forceCleanup) { - if (refCount == 1 || forceCleanup) - { - joinChild.OnRemovedFromDependency(parentTaskOrCollection); + joinChild.OnRemovedFromDependency(parentTaskOrCollection); - data.childDependentNodes.Remove(joinChild); - data.RemoveDependingSynchronousTaskFromChild(joinChild); - parentTaskOrCollection.OnDependencyRemoved(joinChild); + data.childDependentNodes.Remove(joinChild); + data.RemoveDependingSynchronousTaskFromChild(joinChild); + parentTaskOrCollection.OnDependencyRemoved(joinChild); - // A node with no out-going dependency chain cannot be a part of a circular dependency loop. - // JoinableTaskCollection doesn't have a completion event, this logic makes sure it will be removed from a long runnning JTF.Run. - if (data.HasNoChildDependentNode) + // A node with no out-going dependency chain cannot be a part of a circular dependency loop. + // JoinableTaskCollection doesn't have a completion event, this logic makes sure it will be removed from a long runnning JTF.Run. + if (data.HasNoChildDependentNode) + { + DependentSynchronousTask? existingTaskTracking = data.dependingSynchronousTaskTracking; + while (existingTaskTracking is object) { - DependentSynchronousTask? existingTaskTracking = data.dependingSynchronousTaskTracking; - while (existingTaskTracking is object) - { - existingTaskTracking.SynchronousTask.PotentialUnreachableDependents?.Remove(parentTaskOrCollection); - existingTaskTracking = existingTaskTracking.Next; - } + existingTaskTracking.SynchronousTask.PotentialUnreachableDependents?.Remove(parentTaskOrCollection); + existingTaskTracking = existingTaskTracking.Next; } } - else - { - data.childDependentNodes[joinChild] = --refCount; - } + } + else + { + data.childDependentNodes[joinChild] = --refCount; } } } } + } - /// - /// Recursively adds this joinable and all its dependencies to the specified set, that are not yet completed. - /// - /// The current joinableTask or collection contains this data. - /// A collection to hold found. - internal static void AddSelfAndDescendentOrJoinedJobs(IJoinableTaskDependent taskOrCollection, HashSet joinables) - { - Requires.NotNull(taskOrCollection, nameof(taskOrCollection)); - Requires.NotNull(joinables, nameof(joinables)); + /// + /// Recursively adds this joinable and all its dependencies to the specified set, that are not yet completed. + /// + /// The current joinableTask or collection contains this data. + /// A collection to hold found. + internal static void AddSelfAndDescendentOrJoinedJobs(IJoinableTaskDependent taskOrCollection, HashSet joinables) + { + Requires.NotNull(taskOrCollection, nameof(taskOrCollection)); + Requires.NotNull(joinables, nameof(joinables)); - if (taskOrCollection is JoinableTask thisJoinableTask) + if (taskOrCollection is JoinableTask thisJoinableTask) + { + if (thisJoinableTask.IsCompleteRequested) { - if (thisJoinableTask.IsCompleteRequested) + if (!thisJoinableTask.IsFullyCompleted) { - if (!thisJoinableTask.IsFullyCompleted) - { - joinables.Add(thisJoinableTask); - } - - return; + joinables.Add(thisJoinableTask); } - if (!joinables.Add(thisJoinableTask)) - { - return; - } + return; } - Dictionary? childDependentNodes = taskOrCollection.GetJoinableTaskDependentData().childDependentNodes; - if (childDependentNodes is object) + if (!joinables.Add(thisJoinableTask)) { - foreach (KeyValuePair item in childDependentNodes) - { - AddSelfAndDescendentOrJoinedJobs(item.Key, joinables); - } + return; } } - /// - /// When the current dependent node is a synchronous task, this method is called before the thread is blocked to wait it to complete. - /// This adds the current task to the of the task itself (which will propergate through its dependencies.) - /// After the task is finished, is called to revert this change. - /// This method is expected to be used with the JTF lock. - /// - /// The synchronized joinableTask. - /// Return the JoinableTask which has already had pending requests to be handled. - /// The number of pending requests. - internal static void OnSynchronousTaskStartToBlockWaiting(JoinableTask syncTask, out JoinableTask? taskHasPendingRequests, out int pendingRequestsCount) + Dictionary? childDependentNodes = taskOrCollection.GetJoinableTaskDependentData().childDependentNodes; + if (childDependentNodes is object) { - Requires.NotNull(syncTask, nameof(syncTask)); + foreach (KeyValuePair item in childDependentNodes) + { + AddSelfAndDescendentOrJoinedJobs(item.Key, joinables); + } + } + } - pendingRequestsCount = 0; + /// + /// When the current dependent node is a synchronous task, this method is called before the thread is blocked to wait it to complete. + /// This adds the current task to the of the task itself (which will propergate through its dependencies.) + /// After the task is finished, is called to revert this change. + /// This method is expected to be used with the JTF lock. + /// + /// The synchronized joinableTask. + /// Return the JoinableTask which has already had pending requests to be handled. + /// The number of pending requests. + internal static void OnSynchronousTaskStartToBlockWaiting(JoinableTask syncTask, out JoinableTask? taskHasPendingRequests, out int pendingRequestsCount) + { + Requires.NotNull(syncTask, nameof(syncTask)); - taskHasPendingRequests = AddDependingSynchronousTask(syncTask, syncTask, ref pendingRequestsCount); - } + pendingRequestsCount = 0; - /// - /// When the current dependent node is a synchronous task, this method is called after the synchronous is completed, and the thread is no longer blocked. - /// This removes the current task from the of the task itself (and propergate through its dependencies.) - /// It reverts the data structure change done in the . - /// - /// The synchronized joinableTask. - internal static void OnSynchronousTaskEndToBlockWaiting(JoinableTask syncTask) + taskHasPendingRequests = AddDependingSynchronousTask(syncTask, syncTask, ref pendingRequestsCount); + } + + /// + /// When the current dependent node is a synchronous task, this method is called after the synchronous is completed, and the thread is no longer blocked. + /// This removes the current task from the of the task itself (and propergate through its dependencies.) + /// It reverts the data structure change done in the . + /// + /// The synchronized joinableTask. + internal static void OnSynchronousTaskEndToBlockWaiting(JoinableTask syncTask) + { + Requires.NotNull(syncTask, nameof(syncTask)); + using (syncTask.Factory.Context.NoMessagePumpSynchronizationContext.Apply()) { - Requires.NotNull(syncTask, nameof(syncTask)); - using (syncTask.Factory.Context.NoMessagePumpSynchronizationContext.Apply()) + lock (syncTask.Factory.Context.SyncContextLock) { - lock (syncTask.Factory.Context.SyncContextLock) + // Remove itself from the tracking list, after the task is completed. + IJoinableTaskDependent syncTaskItem = syncTask; + if (syncTaskItem.GetJoinableTaskDependentData().dependingSynchronousTaskTracking is object) { - // Remove itself from the tracking list, after the task is completed. - IJoinableTaskDependent syncTaskItem = syncTask; - if (syncTaskItem.GetJoinableTaskDependentData().dependingSynchronousTaskTracking is object) - { - RemoveDependingSynchronousTask(syncTask, syncTask, force: true); - } + RemoveDependingSynchronousTask(syncTask, syncTask, force: true); + } - if (syncTask.PotentialUnreachableDependents is object && syncTask.PotentialUnreachableDependents.Count > 0) - { - RemoveUnreachableDependentItems(syncTask, syncTask.PotentialUnreachableDependents, EmptySet); - syncTask.PotentialUnreachableDependents = null; - } + if (syncTask.PotentialUnreachableDependents is object && syncTask.PotentialUnreachableDependents.Count > 0) + { + RemoveUnreachableDependentItems(syncTask, syncTask.PotentialUnreachableDependents, EmptySet); + syncTask.PotentialUnreachableDependents = null; } } } + } - /// - /// Compute all reachable nodes from a synchronous task. Because we use the result to clean up invalid - /// items from the remain task, we will remove valid task from the collection, and stop immediately if nothing is left. - /// - /// The current joinableTask or collection owns the data. - /// All reachable dependency nodes. This is not a completed list, if there is no remain node. - /// Remain dependency nodes we want to check. After the execution, it will retain non-reachable nodes. - internal static void ComputeSelfAndDescendentOrJoinedJobsAndRemainTasks(IJoinableTaskDependent taskOrCollection, HashSet reachableNodes, HashSet remainNodes) + /// + /// Compute all reachable nodes from a synchronous task. Because we use the result to clean up invalid + /// items from the remain task, we will remove valid task from the collection, and stop immediately if nothing is left. + /// + /// The current joinableTask or collection owns the data. + /// All reachable dependency nodes. This is not a completed list, if there is no remain node. + /// Remain dependency nodes we want to check. After the execution, it will retain non-reachable nodes. + internal static void ComputeSelfAndDescendentOrJoinedJobsAndRemainTasks(IJoinableTaskDependent taskOrCollection, HashSet reachableNodes, HashSet remainNodes) + { + Requires.NotNull(taskOrCollection, nameof(taskOrCollection)); + Requires.NotNull(remainNodes, nameof(remainNodes)); + Requires.NotNull(reachableNodes, nameof(reachableNodes)); + if ((taskOrCollection as JoinableTask)?.IsFullyCompleted != true) { - Requires.NotNull(taskOrCollection, nameof(taskOrCollection)); - Requires.NotNull(remainNodes, nameof(remainNodes)); - Requires.NotNull(reachableNodes, nameof(reachableNodes)); - if ((taskOrCollection as JoinableTask)?.IsFullyCompleted != true) + if (reachableNodes.Add(taskOrCollection)) { - if (reachableNodes.Add(taskOrCollection)) + if (remainNodes.Remove(taskOrCollection) && remainNodes.Count == 0) { - if (remainNodes.Remove(taskOrCollection) && remainNodes.Count == 0) - { - // no remain task left, quit the loop earlier - return; - } + // no remain task left, quit the loop earlier + return; + } - if ((taskOrCollection as JoinableTask)?.IsCompleteRequested == true) - { - return; - } + if ((taskOrCollection as JoinableTask)?.IsCompleteRequested == true) + { + return; + } - Dictionary? dependencies = taskOrCollection.GetJoinableTaskDependentData().childDependentNodes; - if (dependencies is object) + Dictionary? dependencies = taskOrCollection.GetJoinableTaskDependentData().childDependentNodes; + if (dependencies is object) + { + foreach (KeyValuePair item in dependencies) { - foreach (KeyValuePair item in dependencies) + ComputeSelfAndDescendentOrJoinedJobsAndRemainTasks(item.Key, reachableNodes, remainNodes); + if (remainNodes.Count == 0) { - ComputeSelfAndDescendentOrJoinedJobsAndRemainTasks(item.Key, reachableNodes, remainNodes); - if (remainNodes.Count == 0) - { - return; - } + return; } } } } } + } - /// - /// Force to clean up all unreachable dependent item, so they are not marked to block the syncTask. - /// - /// The thread blocking task. - /// Unreachable dependent items. - /// All reachable items. - internal static void RemoveUnreachableDependentItems(JoinableTask syncTask, HashSet unreachableItems, HashSet reachableItemsReadOnlySet) - { - ThreadingEventSource.Instance.CircularJoinableTaskDependencyDetected(unreachableItems.Count, reachableItemsReadOnlySet.Count); + /// + /// Force to clean up all unreachable dependent item, so they are not marked to block the syncTask. + /// + /// The thread blocking task. + /// Unreachable dependent items. + /// All reachable items. + internal static void RemoveUnreachableDependentItems(JoinableTask syncTask, HashSet unreachableItems, HashSet reachableItemsReadOnlySet) + { + ThreadingEventSource.Instance.CircularJoinableTaskDependencyDetected(unreachableItems.Count, reachableItemsReadOnlySet.Count); - HashSet? remainPlaceHold = null; - foreach (IJoinableTaskDependent? unreachableItem in unreachableItems) - { - RemoveDependingSynchronousTask(unreachableItem, syncTask, reachableItemsReadOnlySet, ref remainPlaceHold); - } + HashSet? remainPlaceHold = null; + foreach (IJoinableTaskDependent? unreachableItem in unreachableItems) + { + RemoveDependingSynchronousTask(unreachableItem, syncTask, reachableItemsReadOnlySet, ref remainPlaceHold); } + } - /// - /// Gets all dependent nodes registered in the - /// This method is expected to be used with the JTF lock. - /// - internal IEnumerable GetDirectDependentNodes() + /// + /// Gets all dependent nodes registered in the + /// This method is expected to be used with the JTF lock. + /// + internal IEnumerable GetDirectDependentNodes() + { + if (this.childDependentNodes is null) { - if (this.childDependentNodes is null) - { - return Enumerable.Empty(); - } - - return this.childDependentNodes.Keys; + return Enumerable.Empty(); } - /// - /// Checks whether a dependent node is inside . - /// This method is expected to be used with the JTF lock. - /// - internal bool HasDirectDependency(IJoinableTaskDependent dependency) - { - if (this.childDependentNodes is null) - { - return false; - } + return this.childDependentNodes.Keys; + } - return this.childDependentNodes.ContainsKey(dependency); + /// + /// Checks whether a dependent node is inside . + /// This method is expected to be used with the JTF lock. + /// + internal bool HasDirectDependency(IJoinableTaskDependent dependency) + { + if (this.childDependentNodes is null) + { + return false; } - /// - /// Gets a value indicating whether the main thread is waiting for the task's completion - /// This method is expected to be used with the JTF lock. - /// - internal bool HasMainThreadSynchronousTaskWaiting(IJoinableTaskDependent taskItem) + return this.childDependentNodes.ContainsKey(dependency); + } + + /// + /// Gets a value indicating whether the main thread is waiting for the task's completion + /// This method is expected to be used with the JTF lock. + /// + internal bool HasMainThreadSynchronousTaskWaiting(IJoinableTaskDependent taskItem) + { + DependentSynchronousTask? existingTaskTracking = this.dependingSynchronousTaskTracking; + while (existingTaskTracking is object) { - DependentSynchronousTask? existingTaskTracking = this.dependingSynchronousTaskTracking; - while (existingTaskTracking is object) + DependentSynchronousTask? nextTrackingTask = existingTaskTracking.Next; + if ((existingTaskTracking.SynchronousTask.State & JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread) == JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread) { - DependentSynchronousTask? nextTrackingTask = existingTaskTracking.Next; - if ((existingTaskTracking.SynchronousTask.State & JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread) == JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread) + if (existingTaskTracking.SynchronousTask.HasPotentialUnreachableDependents) { - if (existingTaskTracking.SynchronousTask.HasPotentialUnreachableDependents) - { - // This might remove the current tracking item from the linked list, so we capture next node first. - if (!CleanUpPotentialUnreachableDependentItems(existingTaskTracking.SynchronousTask, out HashSet? allReachableNodes) || - allReachableNodes.Contains(taskItem)) - { - // this task is still a dependenting task - return true; - } - } - else + // This might remove the current tracking item from the linked list, so we capture next node first. + if (!CleanUpPotentialUnreachableDependentItems(existingTaskTracking.SynchronousTask, out HashSet? allReachableNodes) || + allReachableNodes.Contains(taskItem)) { + // this task is still a dependenting task return true; } } - - existingTaskTracking = nextTrackingTask; + else + { + return true; + } } - return false; + existingTaskTracking = nextTrackingTask; } - /// - /// Gets a likely value whether the main thread is blocked for the caller's completion. - /// - internal bool MaybeHasMainThreadSynchronousTaskWaiting() + return false; + } + + /// + /// Gets a likely value whether the main thread is blocked for the caller's completion. + /// + internal bool MaybeHasMainThreadSynchronousTaskWaiting() + { + DependentSynchronousTask? existingTaskTracking = this.dependingSynchronousTaskTracking; + while (existingTaskTracking is object) { - DependentSynchronousTask? existingTaskTracking = this.dependingSynchronousTaskTracking; - while (existingTaskTracking is object) + if ((existingTaskTracking.SynchronousTask.State & JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread) == JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread) { - if ((existingTaskTracking.SynchronousTask.State & JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread) == JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread) - { - return true; - } - - existingTaskTracking = existingTaskTracking.Next; + return true; } - return false; + existingTaskTracking = existingTaskTracking.Next; } - /// - /// Remove all synchronous tasks tracked by the this task. - /// This is called when this task is completed. - /// This method is expected to be used with the JTF lock. - /// - internal void OnTaskCompleted(IJoinableTaskDependent thisDependentNode) + return false; + } + + /// + /// Remove all synchronous tasks tracked by the this task. + /// This is called when this task is completed. + /// This method is expected to be used with the JTF lock. + /// + internal void OnTaskCompleted(IJoinableTaskDependent thisDependentNode) + { + if (this.dependingSynchronousTaskTracking is object) { - if (this.dependingSynchronousTaskTracking is object) - { - DependentSynchronousTask? existingTaskTracking = this.dependingSynchronousTaskTracking; - this.dependingSynchronousTaskTracking = null; + DependentSynchronousTask? existingTaskTracking = this.dependingSynchronousTaskTracking; + this.dependingSynchronousTaskTracking = null; - if (this.childDependentNodes is object) + if (this.childDependentNodes is object) + { + Dictionary.KeyCollection? childrenTasks = this.childDependentNodes.Keys; + while (existingTaskTracking is object) { - Dictionary.KeyCollection? childrenTasks = this.childDependentNodes.Keys; - while (existingTaskTracking is object) - { - RemoveDependingSynchronousTaskFrom(childrenTasks, existingTaskTracking.SynchronousTask, force: existingTaskTracking.SynchronousTask == thisDependentNode); - - HashSet? potentialUnreachableDependents = existingTaskTracking.SynchronousTask.PotentialUnreachableDependents; - if (potentialUnreachableDependents is object && potentialUnreachableDependents.Count > 0) - { - potentialUnreachableDependents.Remove(thisDependentNode); - } + RemoveDependingSynchronousTaskFrom(childrenTasks, existingTaskTracking.SynchronousTask, force: existingTaskTracking.SynchronousTask == thisDependentNode); - existingTaskTracking = existingTaskTracking.Next; + HashSet? potentialUnreachableDependents = existingTaskTracking.SynchronousTask.PotentialUnreachableDependents; + if (potentialUnreachableDependents is object && potentialUnreachableDependents.Count > 0) + { + potentialUnreachableDependents.Remove(thisDependentNode); } + + existingTaskTracking = existingTaskTracking.Next; } } } + } - /// - /// Check whether a task is being tracked in our tracking list. - /// - internal bool IsDependingSynchronousTask(JoinableTask syncTask) + /// + /// Check whether a task is being tracked in our tracking list. + /// + internal bool IsDependingSynchronousTask(JoinableTask syncTask) + { + DependentSynchronousTask? existingTaskTracking = this.dependingSynchronousTaskTracking; + while (existingTaskTracking is object) { - DependentSynchronousTask? existingTaskTracking = this.dependingSynchronousTaskTracking; - while (existingTaskTracking is object) + if (existingTaskTracking.SynchronousTask == syncTask) { - if (existingTaskTracking.SynchronousTask == syncTask) - { - return true; - } - - existingTaskTracking = existingTaskTracking.Next; + return true; } - return false; + existingTaskTracking = existingTaskTracking.Next; } - /// - /// Calculate the collection of events we need trigger after we enqueue a request. - /// This method is expected to be used with the JTF lock. - /// - /// True if we want to find tasks to process the main thread queue. Otherwise tasks to process the background queue. - /// The collection of synchronous tasks we need notify. - internal IReadOnlyCollection GetDependingSynchronousTasks(bool forMainThread) + return false; + } + + /// + /// Calculate the collection of events we need trigger after we enqueue a request. + /// This method is expected to be used with the JTF lock. + /// + /// True if we want to find tasks to process the main thread queue. Otherwise tasks to process the background queue. + /// The collection of synchronous tasks we need notify. + internal IReadOnlyCollection GetDependingSynchronousTasks(bool forMainThread) + { + int count = this.CountOfDependingSynchronousTasks(); + if (count == 0) { - int count = this.CountOfDependingSynchronousTasks(); - if (count == 0) - { - return Array.Empty(); - } + return Array.Empty(); + } - var tasksNeedNotify = new List(count); - DependentSynchronousTask? existingTaskTracking = this.dependingSynchronousTaskTracking; - while (existingTaskTracking is object) + var tasksNeedNotify = new List(count); + DependentSynchronousTask? existingTaskTracking = this.dependingSynchronousTaskTracking; + while (existingTaskTracking is object) + { + JoinableTask? syncTask = existingTaskTracking.SynchronousTask; + bool syncTaskInOnMainThread = (syncTask.State & JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread) == JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread; + if (forMainThread == syncTaskInOnMainThread) { - JoinableTask? syncTask = existingTaskTracking.SynchronousTask; - bool syncTaskInOnMainThread = (syncTask.State & JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread) == JoinableTask.JoinableTaskFlags.SynchronouslyBlockingMainThread; - if (forMainThread == syncTaskInOnMainThread) - { - // Only synchronous tasks are in the list, so we don't need do further check for the CompletingSynchronously flag - tasksNeedNotify.Add(syncTask); - } - - existingTaskTracking = existingTaskTracking.Next; + // Only synchronous tasks are in the list, so we don't need do further check for the CompletingSynchronously flag + tasksNeedNotify.Add(syncTask); } - return tasksNeedNotify; + existingTaskTracking = existingTaskTracking.Next; } - /// - /// Applies all synchronous tasks tracked by this task to a new child/dependent task. - /// - /// The current joinableTask or collection owns the data. - /// The new child task. - /// Pairs of synchronous tasks we need notify and the event source triggering it, plus the number of pending events. - private static IReadOnlyCollection AddDependingSynchronousTaskToChild(IJoinableTaskDependent dependentNode, IJoinableTaskDependent child) + return tasksNeedNotify; + } + + /// + /// Applies all synchronous tasks tracked by this task to a new child/dependent task. + /// + /// The current joinableTask or collection owns the data. + /// The new child task. + /// Pairs of synchronous tasks we need notify and the event source triggering it, plus the number of pending events. + private static IReadOnlyCollection AddDependingSynchronousTaskToChild(IJoinableTaskDependent dependentNode, IJoinableTaskDependent child) + { + Requires.NotNull(dependentNode, nameof(dependentNode)); + Requires.NotNull(child, nameof(child)); + Assumes.True(Monitor.IsEntered(dependentNode.JoinableTaskContext.SyncContextLock)); + + ref JoinableTaskDependentData data = ref dependentNode.GetJoinableTaskDependentData(); + int count = data.CountOfDependingSynchronousTasks(); + if (count == 0) { - Requires.NotNull(dependentNode, nameof(dependentNode)); - Requires.NotNull(child, nameof(child)); - Assumes.True(Monitor.IsEntered(dependentNode.JoinableTaskContext.SyncContextLock)); + return Array.Empty(); + } - ref JoinableTaskDependentData data = ref dependentNode.GetJoinableTaskDependentData(); - int count = data.CountOfDependingSynchronousTasks(); - if (count == 0) + var tasksNeedNotify = new List(count); + DependentSynchronousTask? existingTaskTracking = data.dependingSynchronousTaskTracking; + while (existingTaskTracking is object) + { + int totalEventNumber = 0; + JoinableTask? eventTriggeringTask = AddDependingSynchronousTask(child, existingTaskTracking.SynchronousTask, ref totalEventNumber); + if (eventTriggeringTask is object) { - return Array.Empty(); + tasksNeedNotify.Add(new PendingNotification(existingTaskTracking.SynchronousTask, eventTriggeringTask, totalEventNumber)); } - var tasksNeedNotify = new List(count); - DependentSynchronousTask? existingTaskTracking = data.dependingSynchronousTaskTracking; - while (existingTaskTracking is object) - { - int totalEventNumber = 0; - JoinableTask? eventTriggeringTask = AddDependingSynchronousTask(child, existingTaskTracking.SynchronousTask, ref totalEventNumber); - if (eventTriggeringTask is object) - { - tasksNeedNotify.Add(new PendingNotification(existingTaskTracking.SynchronousTask, eventTriggeringTask, totalEventNumber)); - } + existingTaskTracking = existingTaskTracking.Next; + } - existingTaskTracking = existingTaskTracking.Next; - } + return tasksNeedNotify; + } - return tasksNeedNotify; - } + /// + /// Tracks a new synchronous task for this task. + /// A synchronous task is a task blocking a thread and waits it to be completed. We may want the blocking thread + /// to process events from this task. + /// + /// The current joinableTask or collection. + /// The synchronous task. + /// The total events need be processed. + /// The task causes us to trigger the event of the synchronous task, so it can process new events. Null means we don't need trigger any event. + private static JoinableTask? AddDependingSynchronousTask(IJoinableTaskDependent taskOrCollection, JoinableTask synchronousTask, ref int totalEventsPending) + { + Requires.NotNull(taskOrCollection, nameof(taskOrCollection)); + Requires.NotNull(synchronousTask, nameof(synchronousTask)); + Assumes.True(Monitor.IsEntered(taskOrCollection.JoinableTaskContext.SyncContextLock)); - /// - /// Tracks a new synchronous task for this task. - /// A synchronous task is a task blocking a thread and waits it to be completed. We may want the blocking thread - /// to process events from this task. - /// - /// The current joinableTask or collection. - /// The synchronous task. - /// The total events need be processed. - /// The task causes us to trigger the event of the synchronous task, so it can process new events. Null means we don't need trigger any event. - private static JoinableTask? AddDependingSynchronousTask(IJoinableTaskDependent taskOrCollection, JoinableTask synchronousTask, ref int totalEventsPending) + JoinableTask? thisJoinableTask = taskOrCollection as JoinableTask; + if (thisJoinableTask is object) { - Requires.NotNull(taskOrCollection, nameof(taskOrCollection)); - Requires.NotNull(synchronousTask, nameof(synchronousTask)); - Assumes.True(Monitor.IsEntered(taskOrCollection.JoinableTaskContext.SyncContextLock)); - - JoinableTask? thisJoinableTask = taskOrCollection as JoinableTask; - if (thisJoinableTask is object) + if (thisJoinableTask.IsCompleteRequested) { - if (thisJoinableTask.IsCompleteRequested) + if (!thisJoinableTask.IsFullyCompleted) { - if (!thisJoinableTask.IsFullyCompleted) + // A completed task might still have pending items in the queue. + int pendingCount = thisJoinableTask.GetPendingEventCountForSynchronousTask(synchronousTask); + if (pendingCount > 0) { - // A completed task might still have pending items in the queue. - int pendingCount = thisJoinableTask.GetPendingEventCountForSynchronousTask(synchronousTask); - if (pendingCount > 0) - { - totalEventsPending += pendingCount; - return thisJoinableTask; - } + totalEventsPending += pendingCount; + return thisJoinableTask; } - - return null; } + + return null; } + } - ref JoinableTaskDependentData data = ref taskOrCollection.GetJoinableTaskDependentData(); - DependentSynchronousTask? existingTaskTracking = data.dependingSynchronousTaskTracking; - while (existingTaskTracking is object) + ref JoinableTaskDependentData data = ref taskOrCollection.GetJoinableTaskDependentData(); + DependentSynchronousTask? existingTaskTracking = data.dependingSynchronousTaskTracking; + while (existingTaskTracking is object) + { + if (existingTaskTracking.SynchronousTask == synchronousTask) { - if (existingTaskTracking.SynchronousTask == synchronousTask) - { - existingTaskTracking.ReferenceCount++; - return null; - } - - existingTaskTracking = existingTaskTracking.Next; + existingTaskTracking.ReferenceCount++; + return null; } - JoinableTask? eventTriggeringTask = null; + existingTaskTracking = existingTaskTracking.Next; + } + + JoinableTask? eventTriggeringTask = null; - if (thisJoinableTask is object) + if (thisJoinableTask is object) + { + int pendingItemCount = thisJoinableTask.GetPendingEventCountForSynchronousTask(synchronousTask); + if (pendingItemCount > 0) { - int pendingItemCount = thisJoinableTask.GetPendingEventCountForSynchronousTask(synchronousTask); - if (pendingItemCount > 0) - { - totalEventsPending += pendingItemCount; - eventTriggeringTask = thisJoinableTask; - } + totalEventsPending += pendingItemCount; + eventTriggeringTask = thisJoinableTask; } + } - // For a new synchronous task, we need apply it to our child tasks. - var newTaskTracking = new DependentSynchronousTask(synchronousTask) - { - Next = data.dependingSynchronousTaskTracking, - }; + // For a new synchronous task, we need apply it to our child tasks. + var newTaskTracking = new DependentSynchronousTask(synchronousTask) + { + Next = data.dependingSynchronousTaskTracking, + }; - Thread.MemoryBarrier(); + Thread.MemoryBarrier(); - data.dependingSynchronousTaskTracking = newTaskTracking; + data.dependingSynchronousTaskTracking = newTaskTracking; - if (data.childDependentNodes is object) + if (data.childDependentNodes is object) + { + foreach (KeyValuePair item in data.childDependentNodes) { - foreach (KeyValuePair item in data.childDependentNodes) + JoinableTask? childTiggeringTask = AddDependingSynchronousTask(item.Key, synchronousTask, ref totalEventsPending); + if (eventTriggeringTask is null) { - JoinableTask? childTiggeringTask = AddDependingSynchronousTask(item.Key, synchronousTask, ref totalEventsPending); - if (eventTriggeringTask is null) - { - eventTriggeringTask = childTiggeringTask; - } + eventTriggeringTask = childTiggeringTask; } } - - return eventTriggeringTask; } - /// - /// Remove a synchronous task from the tracking list. - /// - /// The current joinableTask or collection. - /// The synchronous task. - /// We always remove it from the tracking list if it is true. Otherwise, we keep tracking the reference count. - private static void RemoveDependingSynchronousTask(IJoinableTaskDependent taskOrCollection, JoinableTask syncTask, bool force = false) - { - Requires.NotNull(taskOrCollection, nameof(taskOrCollection)); - Requires.NotNull(syncTask, nameof(syncTask)); - Assumes.True(Monitor.IsEntered(taskOrCollection.JoinableTaskContext.SyncContextLock)); + return eventTriggeringTask; + } - RemoveDependingSynchronousTaskFrom(new IJoinableTaskDependent[] { taskOrCollection }, syncTask, force); - } + /// + /// Remove a synchronous task from the tracking list. + /// + /// The current joinableTask or collection. + /// The synchronous task. + /// We always remove it from the tracking list if it is true. Otherwise, we keep tracking the reference count. + private static void RemoveDependingSynchronousTask(IJoinableTaskDependent taskOrCollection, JoinableTask syncTask, bool force = false) + { + Requires.NotNull(taskOrCollection, nameof(taskOrCollection)); + Requires.NotNull(syncTask, nameof(syncTask)); + Assumes.True(Monitor.IsEntered(taskOrCollection.JoinableTaskContext.SyncContextLock)); - /// - /// Remove a synchronous task from the tracking list of a list of tasks. - /// - /// A list of tasks we need update the tracking list. - /// The synchronous task we want to remove. - /// We always remove it from the tracking list if it is true. Otherwise, we keep tracking the reference count. - private static void RemoveDependingSynchronousTaskFrom(IReadOnlyCollection tasks, JoinableTask syncTask, bool force) - { - Requires.NotNull(tasks, nameof(tasks)); - Requires.NotNull(syncTask, nameof(syncTask)); + RemoveDependingSynchronousTaskFrom(new IJoinableTaskDependent[] { taskOrCollection }, syncTask, force); + } - HashSet? emptySetOrNull = force ? EmptySet : null; - HashSet? remainNodes = syncTask.PotentialUnreachableDependents; + /// + /// Remove a synchronous task from the tracking list of a list of tasks. + /// + /// A list of tasks we need update the tracking list. + /// The synchronous task we want to remove. + /// We always remove it from the tracking list if it is true. Otherwise, we keep tracking the reference count. + private static void RemoveDependingSynchronousTaskFrom(IReadOnlyCollection tasks, JoinableTask syncTask, bool force) + { + Requires.NotNull(tasks, nameof(tasks)); + Requires.NotNull(syncTask, nameof(syncTask)); - foreach (IJoinableTaskDependent? task in tasks) - { - RemoveDependingSynchronousTask(task, syncTask, reachableNodesReadOnlySet: emptySetOrNull, ref remainNodes); - } + HashSet? emptySetOrNull = force ? EmptySet : null; + HashSet? remainNodes = syncTask.PotentialUnreachableDependents; - if (remainNodes is object && remainNodes.Count > 0) + foreach (IJoinableTaskDependent? task in tasks) + { + RemoveDependingSynchronousTask(task, syncTask, reachableNodesReadOnlySet: emptySetOrNull, ref remainNodes); + } + + if (remainNodes is object && remainNodes.Count > 0) + { + if (force) { - if (force) - { - Assumes.NotNull(emptySetOrNull); - Assumes.True(emptySetOrNull.Count == 0); + Assumes.NotNull(emptySetOrNull); + Assumes.True(emptySetOrNull.Count == 0); - RemoveUnreachableDependentItems(syncTask, remainNodes, reachableItemsReadOnlySet: emptySetOrNull); + RemoveUnreachableDependentItems(syncTask, remainNodes, reachableItemsReadOnlySet: emptySetOrNull); - syncTask.PotentialUnreachableDependents = null; - } - else if (syncTask.PotentialUnreachableDependents != remainNodes) - { - // a set of tasks may form a dependent loop, so it will make the reference count system - // not to work correctly when we try to remove the synchronous task. - // It will require full dependency scanning to clean them up, which is quite expensive, - // so we keep tracking them, and clean them up when it becomes essential. - syncTask.PotentialUnreachableDependents = remainNodes; - } + syncTask.PotentialUnreachableDependents = null; + } + else if (syncTask.PotentialUnreachableDependents != remainNodes) + { + // a set of tasks may form a dependent loop, so it will make the reference count system + // not to work correctly when we try to remove the synchronous task. + // It will require full dependency scanning to clean them up, which is quite expensive, + // so we keep tracking them, and clean them up when it becomes essential. + syncTask.PotentialUnreachableDependents = remainNodes; } } + } - /// - /// Remove a synchronous task from the tracking list of this task. - /// - /// The current joinableTask or collection. - /// The synchronous task. - /// - /// If it is not null, it will contain all dependency nodes which can track the synchronous task. We will ignore reference count in that case. - /// - /// This will retain the tasks which still tracks the synchronous task. - private static void RemoveDependingSynchronousTask(IJoinableTaskDependent taskOrCollection, JoinableTask task, HashSet? reachableNodesReadOnlySet, ref HashSet? remainingDependentNodes) - { - Requires.NotNull(taskOrCollection, nameof(taskOrCollection)); - Requires.NotNull(task, nameof(task)); + /// + /// Remove a synchronous task from the tracking list of this task. + /// + /// The current joinableTask or collection. + /// The synchronous task. + /// + /// If it is not null, it will contain all dependency nodes which can track the synchronous task. We will ignore reference count in that case. + /// + /// This will retain the tasks which still tracks the synchronous task. + private static void RemoveDependingSynchronousTask(IJoinableTaskDependent taskOrCollection, JoinableTask task, HashSet? reachableNodesReadOnlySet, ref HashSet? remainingDependentNodes) + { + Requires.NotNull(taskOrCollection, nameof(taskOrCollection)); + Requires.NotNull(task, nameof(task)); - ref JoinableTaskDependentData data = ref taskOrCollection.GetJoinableTaskDependentData(); - DependentSynchronousTask? previousTaskTracking = null; - DependentSynchronousTask? currentTaskTracking = data.dependingSynchronousTaskTracking; - bool removed = false; + ref JoinableTaskDependentData data = ref taskOrCollection.GetJoinableTaskDependentData(); + DependentSynchronousTask? previousTaskTracking = null; + DependentSynchronousTask? currentTaskTracking = data.dependingSynchronousTaskTracking; + bool removed = false; - while (currentTaskTracking is object) + while (currentTaskTracking is object) + { + if (currentTaskTracking.SynchronousTask == task) { - if (currentTaskTracking.SynchronousTask == task) + if (--currentTaskTracking.ReferenceCount > 0) { - if (--currentTaskTracking.ReferenceCount > 0) + if (reachableNodesReadOnlySet is object) { - if (reachableNodesReadOnlySet is object) + if (!reachableNodesReadOnlySet.Contains(taskOrCollection)) { - if (!reachableNodesReadOnlySet.Contains(taskOrCollection)) - { - currentTaskTracking.ReferenceCount = 0; - } + currentTaskTracking.ReferenceCount = 0; } } + } - if (currentTaskTracking.ReferenceCount == 0) + if (currentTaskTracking.ReferenceCount == 0) + { + removed = true; + if (previousTaskTracking is object) { - removed = true; - if (previousTaskTracking is object) - { - previousTaskTracking.Next = currentTaskTracking.Next; - } - else - { - data.dependingSynchronousTaskTracking = currentTaskTracking.Next; - } + previousTaskTracking.Next = currentTaskTracking.Next; + } + else + { + data.dependingSynchronousTaskTracking = currentTaskTracking.Next; } + } - if (reachableNodesReadOnlySet is null) + if (reachableNodesReadOnlySet is null) + { + // if a node doesn't have dependencies, it cannot be a part of a dependency circle. + if (removed || taskOrCollection.GetJoinableTaskDependentData().HasNoChildDependentNode) { - // if a node doesn't have dependencies, it cannot be a part of a dependency circle. - if (removed || taskOrCollection.GetJoinableTaskDependentData().HasNoChildDependentNode) + if (remainingDependentNodes is object) { - if (remainingDependentNodes is object) - { - remainingDependentNodes.Remove(taskOrCollection); - } + remainingDependentNodes.Remove(taskOrCollection); } - else + } + else + { + if (remainingDependentNodes is null) { - if (remainingDependentNodes is null) - { - remainingDependentNodes = new HashSet(); - } - - remainingDependentNodes.Add(taskOrCollection); + remainingDependentNodes = new HashSet(); } - } - break; + remainingDependentNodes.Add(taskOrCollection); + } } - previousTaskTracking = currentTaskTracking; - currentTaskTracking = currentTaskTracking.Next; + break; } - if (removed && data.childDependentNodes is object) - { - foreach (KeyValuePair item in data.childDependentNodes) - { - RemoveDependingSynchronousTask(item.Key, task, reachableNodesReadOnlySet, ref remainingDependentNodes); - } - } + previousTaskTracking = currentTaskTracking; + currentTaskTracking = currentTaskTracking.Next; } - /// - /// Get how many number of synchronous tasks in our tracking list. - /// - private int CountOfDependingSynchronousTasks() + if (removed && data.childDependentNodes is object) { - int count = 0; - DependentSynchronousTask? existingTaskTracking = this.dependingSynchronousTaskTracking; - while (existingTaskTracking is object) + foreach (KeyValuePair item in data.childDependentNodes) { - count++; - existingTaskTracking = existingTaskTracking.Next; + RemoveDependingSynchronousTask(item.Key, task, reachableNodesReadOnlySet, ref remainingDependentNodes); } + } + } - return count; + /// + /// Get how many number of synchronous tasks in our tracking list. + /// + private int CountOfDependingSynchronousTasks() + { + int count = 0; + DependentSynchronousTask? existingTaskTracking = this.dependingSynchronousTaskTracking; + while (existingTaskTracking is object) + { + count++; + existingTaskTracking = existingTaskTracking.Next; } - /// - /// Removes all synchronous tasks we applies to a dependent task, after the relationship is removed. - /// - /// The original dependent task. - private void RemoveDependingSynchronousTaskFromChild(IJoinableTaskDependent child) + return count; + } + + /// + /// Removes all synchronous tasks we applies to a dependent task, after the relationship is removed. + /// + /// The original dependent task. + private void RemoveDependingSynchronousTaskFromChild(IJoinableTaskDependent child) + { + Requires.NotNull(child, nameof(child)); + + DependentSynchronousTask? existingTaskTracking = this.dependingSynchronousTaskTracking; + while (existingTaskTracking is object) + { + RemoveDependingSynchronousTask(child, existingTaskTracking.SynchronousTask); + existingTaskTracking = existingTaskTracking.Next; + } + } + + /// + /// The record of a pending notification we need send to the synchronous task that we have some new messages to process. + /// + private readonly struct PendingNotification + { + internal PendingNotification(JoinableTask synchronousTask, JoinableTask taskHasPendingMessages, int newPendingMessagesCount) { - Requires.NotNull(child, nameof(child)); + Requires.NotNull(synchronousTask, nameof(synchronousTask)); + Requires.NotNull(taskHasPendingMessages, nameof(taskHasPendingMessages)); - DependentSynchronousTask? existingTaskTracking = this.dependingSynchronousTaskTracking; - while (existingTaskTracking is object) - { - RemoveDependingSynchronousTask(child, existingTaskTracking.SynchronousTask); - existingTaskTracking = existingTaskTracking.Next; - } + this.SynchronousTask = synchronousTask; + this.TaskHasPendingMessages = taskHasPendingMessages; + this.NewPendingMessagesCount = newPendingMessagesCount; } /// - /// The record of a pending notification we need send to the synchronous task that we have some new messages to process. + /// Gets the synchronous task which need process new messages. /// - private readonly struct PendingNotification - { - internal PendingNotification(JoinableTask synchronousTask, JoinableTask taskHasPendingMessages, int newPendingMessagesCount) - { - Requires.NotNull(synchronousTask, nameof(synchronousTask)); - Requires.NotNull(taskHasPendingMessages, nameof(taskHasPendingMessages)); + internal JoinableTask SynchronousTask { get; } - this.SynchronousTask = synchronousTask; - this.TaskHasPendingMessages = taskHasPendingMessages; - this.NewPendingMessagesCount = newPendingMessagesCount; - } - - /// - /// Gets the synchronous task which need process new messages. - /// - internal JoinableTask SynchronousTask { get; } - - /// - /// Gets one JoinableTask which may have pending messages. We may have multiple new JoinableTasks which contains pending messages. - /// This is just one of them. It gives the synchronous task a way to start quickly without searching all messages. - /// - internal JoinableTask TaskHasPendingMessages { get; } - - /// - /// Gets the total number of new pending messages. The real number could be less than that, but should not be more than that. - /// - internal int NewPendingMessagesCount { get; } - } + /// + /// Gets one JoinableTask which may have pending messages. We may have multiple new JoinableTasks which contains pending messages. + /// This is just one of them. It gives the synchronous task a way to start quickly without searching all messages. + /// + internal JoinableTask TaskHasPendingMessages { get; } /// - /// A single linked list to maintain synchronous JoinableTask depends on the current task, - /// which may process the queue of the current task. + /// Gets the total number of new pending messages. The real number could be less than that, but should not be more than that. /// - private class DependentSynchronousTask + internal int NewPendingMessagesCount { get; } + } + + /// + /// A single linked list to maintain synchronous JoinableTask depends on the current task, + /// which may process the queue of the current task. + /// + private class DependentSynchronousTask + { + internal DependentSynchronousTask(JoinableTask task) { - internal DependentSynchronousTask(JoinableTask task) - { - this.SynchronousTask = task; - this.ReferenceCount = 1; - } + this.SynchronousTask = task; + this.ReferenceCount = 1; + } - /// - /// Gets or sets the chain of the single linked list. - /// - internal DependentSynchronousTask? Next { get; set; } + /// + /// Gets or sets the chain of the single linked list. + /// + internal DependentSynchronousTask? Next { get; set; } - /// - /// Gets the synchronous task. - /// - internal JoinableTask SynchronousTask { get; } + /// + /// Gets the synchronous task. + /// + internal JoinableTask SynchronousTask { get; } - /// - /// Gets or sets the reference count. We remove the item from the list, if it reaches 0. - /// - internal int ReferenceCount { get; set; } - } + /// + /// Gets or sets the reference count. We remove the item from the list, if it reaches 0. + /// + internal int ReferenceCount { get; set; } } } } diff --git a/src/Microsoft.VisualStudio.Threading/JoinableTaskFactory.cs b/src/Microsoft.VisualStudio.Threading/JoinableTaskFactory.cs index ab6d38c4d..b6bc0acfd 100644 --- a/src/Microsoft.VisualStudio.Threading/JoinableTaskFactory.cs +++ b/src/Microsoft.VisualStudio.Threading/JoinableTaskFactory.cs @@ -1,1028 +1,1112 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using JoinableTaskSynchronizationContext = Microsoft.VisualStudio.Threading.JoinableTask.JoinableTaskSynchronizationContext; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// A factory for starting asynchronous tasks that can mitigate deadlocks +/// when the tasks require the Main thread of an application and the Main +/// thread may itself be blocking on the completion of a task. +/// +/// +/// For more complete comments please see the . +/// +public partial class JoinableTaskFactory { - using System; - using System.Collections; - using System.Collections.Generic; - using System.Diagnostics; - using System.Diagnostics.CodeAnalysis; - using System.Globalization; - using System.Linq; - using System.Reflection; - using System.Runtime.CompilerServices; - using System.Security; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - using JoinableTaskSynchronizationContext = Microsoft.VisualStudio.Threading.JoinableTask.JoinableTaskSynchronizationContext; + /// + /// The that owns this instance. + /// + private readonly JoinableTaskContext owner; + + private readonly SynchronizationContext? mainThreadJobSyncContext; /// - /// A factory for starting asynchronous tasks that can mitigate deadlocks - /// when the tasks require the Main thread of an application and the Main - /// thread may itself be blocking on the completion of a task. + /// The collection to add all created tasks to. May be . /// - /// - /// For more complete comments please see the . - /// - public partial class JoinableTaskFactory + private readonly JoinableTaskCollection? jobCollection; + + /// + /// Backing field for the property. + /// + private TimeSpan hangDetectionTimeout = TimeSpan.FromSeconds(6); + + /// + /// Initializes a new instance of the class. + /// + /// The context for the tasks created by this factory. + public JoinableTaskFactory(JoinableTaskContext owner) + : this(owner, null) { - /// - /// The that owns this instance. - /// - private readonly JoinableTaskContext owner; + } - private readonly SynchronizationContext mainThreadJobSyncContext; + /// + /// Initializes a new instance of the class + /// that adds all generated jobs to the specified collection. + /// + /// The collection that all tasks created by this factory will belong to till they complete. + public JoinableTaskFactory(JoinableTaskCollection collection) + : this(Requires.NotNull(collection, "collection").Context, collection) + { + } - /// - /// The collection to add all created tasks to. May be null. - /// - private readonly JoinableTaskCollection? jobCollection; + /// + /// Initializes a new instance of the class. + /// + /// The context for the tasks created by this factory. + /// The collection that all tasks created by this factory will belong to till they complete. May be null. + internal JoinableTaskFactory(JoinableTaskContext owner, JoinableTaskCollection? collection) + { + Requires.NotNull(owner, nameof(owner)); + Assumes.True(collection is null || collection.Context == owner); - /// - /// Backing field for the property. - /// - private TimeSpan hangDetectionTimeout = TimeSpan.FromSeconds(6); + this.owner = owner; + this.jobCollection = collection; + this.mainThreadJobSyncContext = owner.IsNoOpContext ? null : new JoinableTaskSynchronizationContext(this); + } - /// - /// Initializes a new instance of the class. - /// - /// The context for the tasks created by this factory. - public JoinableTaskFactory(JoinableTaskContext owner) - : this(owner, null) - { - } + /// + /// Gets the joinable task context to which this factory belongs. + /// + public JoinableTaskContext Context + { + get { return this.owner; } + } - /// - /// Initializes a new instance of the class - /// that adds all generated jobs to the specified collection. - /// - /// The collection that all tasks created by this factory will belong to till they complete. - public JoinableTaskFactory(JoinableTaskCollection collection) - : this(Requires.NotNull(collection, "collection").Context, collection) - { - } + /// + /// Gets the synchronization context to apply before executing work associated with this factory. + /// + internal SynchronizationContext? ApplicableJobSyncContext + { + get { return this.Context.IsOnMainThread ? this.mainThreadJobSyncContext : null; } + } - /// - /// Initializes a new instance of the class. - /// - /// The context for the tasks created by this factory. - /// The collection that all tasks created by this factory will belong to till they complete. May be null. - internal JoinableTaskFactory(JoinableTaskContext owner, JoinableTaskCollection? collection) - { - Requires.NotNull(owner, nameof(owner)); - Assumes.True(collection is null || collection.Context == owner); + /// + /// Gets the collection to which created tasks belong until they complete. May be null. + /// + internal JoinableTaskCollection? Collection + { + get { return this.jobCollection; } + } - this.owner = owner; - this.jobCollection = collection; - this.mainThreadJobSyncContext = new JoinableTaskSynchronizationContext(this); - } + /// + /// Gets a on which + /// should be called from + /// when has not been called. + /// + /// + /// This allows a WPF-aware -derived class within this assembly + /// to match Dispatcher.DisableProcessing() behavior. + /// + internal SynchronizationContext? DefaultWaitPolicy { get; init; } - /// - /// Gets the joinable task context to which this factory belongs. - /// - public JoinableTaskContext Context + /// + /// Gets or sets the timeout after which no activity while synchronously blocking + /// suggests a hang has occurred. + /// + protected TimeSpan HangDetectionTimeout + { + get { - get { return this.owner; } + return this.hangDetectionTimeout; } - /// - /// Gets the synchronization context to apply before executing work associated with this factory. - /// - internal SynchronizationContext? ApplicableJobSyncContext + set { - get { return this.Context.IsOnMainThread ? this.mainThreadJobSyncContext : null; } + Requires.Range(value > TimeSpan.Zero, "value"); + this.hangDetectionTimeout = value; } + } - /// - /// Gets the collection to which created tasks belong until they complete. May be null. - /// - internal JoinableTaskCollection? Collection - { - get { return this.jobCollection; } - } + /// + /// Gets the underlying that controls the main thread in the host. + /// + protected SynchronizationContext? UnderlyingSynchronizationContext + { + get { return this.Context.UnderlyingSynchronizationContext; } + } - /// - /// Gets or sets the timeout after which no activity while synchronously blocking - /// suggests a hang has occurred. - /// - protected TimeSpan HangDetectionTimeout + /// + /// Gets an awaitable whose continuations execute on the main thread, + /// in such a way as to mitigate both deadlocks and reentrancy. + /// + /// + /// A token whose cancellation will immediately schedule the continuation + /// on a threadpool thread and will cause the continuation to throw , + /// even if the caller is already on the main thread. + /// + /// An awaitable. + /// + /// Thrown back at the awaiting caller if is canceled, + /// even if the caller is already on the main thread. + /// + /// + /// + /// + /// private async Task SomeOperationAsync() { + /// // on the caller's thread. + /// await DoAsync(); + /// + /// // Now switch to a threadpool thread explicitly. + /// await TaskScheduler.Default; + /// + /// // Now switch to the Main thread to talk to some STA object. + /// await this.JobContext.SwitchToMainThreadAsync(); + /// STAService.DoSomething(); + /// } + /// + /// + /// + /// When the owning is created with a , + /// this method has no effect and the caller will continue execution on its original thread. + /// + /// + public MainThreadAwaitable SwitchToMainThreadAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + return new MainThreadAwaitable(this, this.Context.AmbientTask, cancellationToken); + } + + /// + /// Gets an awaitable whose continuations execute on the synchronization context that this instance was initialized with, + /// in such a way as to mitigate both deadlocks and reentrancy. + /// + /// A value indicating whether the caller should yield even if + /// already executing on the main thread. + /// + /// A token whose cancellation will immediately schedule the continuation + /// on a threadpool thread and will cause the continuation to throw , + /// even if the caller is already on the main thread. + /// + /// An awaitable. + /// + /// Thrown back at the awaiting caller if is canceled, + /// even if the caller is already on the main thread. + /// + /// + /// + /// + /// private async Task SomeOperationAsync() + /// { + /// // This first part can be on the caller's thread, whatever that is. + /// DoSomething(); + /// + /// // Now switch to the Main thread to talk to some STA object. + /// // Supposing it is also important to *not* do this step on our caller's callstack, + /// // be sure we yield even if we're on the UI thread. + /// await this.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true); + /// STAService.DoSomething(); + /// } + /// + /// + /// + public MainThreadAwaitable SwitchToMainThreadAsync(bool alwaysYield, CancellationToken cancellationToken = default(CancellationToken)) + { + return new MainThreadAwaitable(this, this.Context.AmbientTask, cancellationToken, alwaysYield); + } + + /// + public void Run(Func asyncMethod) + { + this.Run(asyncMethod, JoinableTaskCreationOptions.None, entrypointOverride: null); + } + + /// + public void Run(Func asyncMethod, JoinableTaskCreationOptions creationOptions) + { + this.Run(asyncMethod, creationOptions, entrypointOverride: null); + } + + /// + public T Run(Func> asyncMethod) + { + return this.Run(asyncMethod, JoinableTaskCreationOptions.None); + } + + /// + /// Runs the specified asynchronous method to completion while synchronously blocking the calling thread. + /// + /// The type of value returned by the asynchronous operation. + /// The asynchronous method to execute. + /// The used to customize the task's behavior. + /// The result of the Task returned by . + /// + /// Any exception thrown by the delegate is rethrown in its original type to the caller of this method. + /// When the delegate resumes from a yielding await, the default behavior is to resume in its original context + /// as an ordinary async method execution would. For example, if the caller was on the main thread, execution + /// resumes after an await on the main thread; but if it started on a threadpool thread it resumes on a threadpool thread. + /// + /// + /// + /// + public T Run(Func> asyncMethod, JoinableTaskCreationOptions creationOptions) + { + VerifyNoNonConcurrentSyncContext(); + JoinableTask? joinable = this.RunAsync(asyncMethod, synchronouslyBlocking: true, parentToken: null, creationOptions: creationOptions); + return joinable.CompleteOnCurrentThread(); + } + + /// + public JoinableTask RunAsync(Func asyncMethod) + { + return this.RunAsync(asyncMethod, synchronouslyBlocking: false, parentToken: null, creationOptions: JoinableTaskCreationOptions.None); + } + + /// + public JoinableTask RunAsync(Func asyncMethod, JoinableTaskCreationOptions creationOptions) + { + return this.RunAsync(asyncMethod, synchronouslyBlocking: false, parentToken: null, creationOptions: creationOptions); + } + + /// + public JoinableTask RunAsync(Func asyncMethod, string? parentToken, JoinableTaskCreationOptions creationOptions) + { + return this.RunAsync(asyncMethod, synchronouslyBlocking: false, parentToken, creationOptions: creationOptions); + } + + /// + public JoinableTask RunAsync(Func> asyncMethod) + { + return this.RunAsync(asyncMethod, synchronouslyBlocking: false, parentToken: null, creationOptions: JoinableTaskCreationOptions.None); + } + + /// + public JoinableTask RunAsync(Func> asyncMethod, JoinableTaskCreationOptions creationOptions) + { + return this.RunAsync(asyncMethod, synchronouslyBlocking: false, parentToken: null, creationOptions: creationOptions); + } + + /// + public JoinableTask RunAsync(Func> asyncMethod, string? parentToken, JoinableTaskCreationOptions creationOptions) + { + return this.RunAsync(asyncMethod, synchronouslyBlocking: false, parentToken, creationOptions: creationOptions); + } + +#pragma warning disable SA1629 // Documentation text should end with a period + /// + /// Prevents filtered message pumps from running during synchronous waits for the ambient . + /// + /// + /// A value that may be disposed of when the need to suppress synchronous wait message pumps is ended. + /// Alternatively it may be discarded if the rest of the is intended to have processing disabled. + /// + /// Thrown when called outside the context of a . + /// + /// + /// During a yielding within a , no message pump ever runs + /// regardless of whether this method is called, except for the internal one that lets in only relevant work. + /// When user code runs within the delegate or its callees that ends up requiring + /// a synchronous block of the main thread (e.g. synchronous I/O or lock contention), this wait is typically + /// implemented by calling on . + /// The default implementation of this method allows for certain interruptions (e.g. COM RPC calls), which + /// may avoid deadlocks in certain situations. + /// + /// + /// Calling this method will replace the default implementation of + /// with one that will not allow such interruptions while that is active and in control of + /// . + /// As this method may be called multiple times, this effect remains on the target + /// until all invocations' return values are disposed (in any order). + /// The effect only applies to the direct . It does not affect any of its children or parents. + /// + /// + /// Disabling processing has no effect on non-Windows operating systems. + /// + /// + /// Disposing the resulting value will revert to the default behavior. + /// Callers need not ever dispose of this value if the intent is to disable processing for the remainder of that + /// 's execution. + /// + /// + /// + /// + /// Here is a simple, common usage of this method: + /// + /// + /// + /// Following are more examples of how it might be used: + /// + /// + /// + public ProcessingDisabledOperation DisableProcessing() => new(this.Context.AmbientTask ?? throw new InvalidOperationException(Strings.NoAmbientTask)); +#pragma warning restore SA1629 // Documentation text should end with a period + + /// + /// Responds to calls to + /// by scheduling a continuation to execute on the Main thread. + /// + /// The callback to invoke. + internal SingleExecuteProtector RequestSwitchToMainThread(Action callback) + { + Requires.NotNull(callback, nameof(callback)); + + // Make sure that this thread switch request is in a job that is captured by the job collection + // to which this switch request belongs. + // If an ambient job already exists and belongs to the collection, that's good enough. But if + // there is no ambient job, or the ambient job does not belong to the collection, we must create + // a (child) job and add that to this job factory's collection so that folks joining that factory + // can help this switch to complete. + JoinableTask? ambientJob = this.Context.AmbientTask; + SingleExecuteProtector? wrapper = null; + if (ambientJob is null || (this.jobCollection is object && !this.jobCollection.Contains(ambientJob))) { - get - { - return this.hangDetectionTimeout; - } + JoinableTask? transient = this.RunAsync( + delegate + { + RoslynDebug.Assert(this.Context.AmbientTask is object, $"{nameof(this.Context.AmbientTask)} is always set for {nameof(this.RunAsync)} callbacks."); + + ambientJob = this.Context.AmbientTask; + wrapper = SingleExecuteProtector.Create(ambientJob, callback); + ambientJob.Post(SingleExecuteProtector.ExecuteOnce, wrapper, true); + return Task.CompletedTask; + }, + synchronouslyBlocking: false, + parentToken: null, + creationOptions: JoinableTaskCreationOptions.None, + entrypointOverride: callback); - set + if (transient.Task.IsFaulted) { - Requires.Range(value > TimeSpan.Zero, "value"); - this.hangDetectionTimeout = value; + // rethrow the exception. + transient.Task.GetAwaiter().GetResult(); } } - - /// - /// Gets the underlying that controls the main thread in the host. - /// - protected SynchronizationContext? UnderlyingSynchronizationContext + else { - get { return this.Context.UnderlyingSynchronizationContext; } + wrapper = SingleExecuteProtector.Create(ambientJob, callback); + ambientJob.Post(SingleExecuteProtector.ExecuteOnce, wrapper, true); } - /// - /// Gets an awaitable whose continuations execute on the synchronization context that this instance was initialized with, - /// in such a way as to mitigate both deadlocks and reentrancy. - /// - /// - /// A token whose cancellation will immediately schedule the continuation - /// on a threadpool thread and will cause the continuation to throw , - /// even if the caller is already on the main thread. - /// - /// An awaitable. - /// - /// Thrown back at the awaiting caller if is canceled, - /// even if the caller is already on the main thread. - /// - /// - /// - /// - /// private async Task SomeOperationAsync() { - /// // on the caller's thread. - /// await DoAsync(); - /// - /// // Now switch to a threadpool thread explicitly. - /// await TaskScheduler.Default; - /// - /// // Now switch to the Main thread to talk to some STA object. - /// await this.JobContext.SwitchToMainThreadAsync(); - /// STAService.DoSomething(); - /// } - /// - /// - /// - public MainThreadAwaitable SwitchToMainThreadAsync(CancellationToken cancellationToken = default(CancellationToken)) - { - return new MainThreadAwaitable(this, this.Context.AmbientTask, cancellationToken); - } + Assumes.NotNull(wrapper); + return wrapper; + } - /// - /// Gets an awaitable whose continuations execute on the synchronization context that this instance was initialized with, - /// in such a way as to mitigate both deadlocks and reentrancy. - /// - /// A value indicating whether the caller should yield even if - /// already executing on the main thread. - /// - /// A token whose cancellation will immediately schedule the continuation - /// on a threadpool thread and will cause the continuation to throw , - /// even if the caller is already on the main thread. - /// - /// An awaitable. - /// - /// Thrown back at the awaiting caller if is canceled, - /// even if the caller is already on the main thread. - /// - /// - /// - /// - /// private async Task SomeOperationAsync() - /// { - /// // This first part can be on the caller's thread, whatever that is. - /// DoSomething(); - /// - /// // Now switch to the Main thread to talk to some STA object. - /// // Supposing it is also important to *not* do this step on our caller's callstack, - /// // be sure we yield even if we're on the UI thread. - /// await this.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true); - /// STAService.DoSomething(); - /// } - /// - /// - /// - public MainThreadAwaitable SwitchToMainThreadAsync(bool alwaysYield, CancellationToken cancellationToken = default(CancellationToken)) - { - return new MainThreadAwaitable(this, this.Context.AmbientTask, cancellationToken, alwaysYield); - } + /// + /// Posts a callback to the main thread via the underlying dispatcher, + /// or to the threadpool when no dispatcher exists on the main thread. + /// + internal void PostToUnderlyingSynchronizationContextOrThreadPool(SingleExecuteProtector callback) + { + Requires.NotNull(callback, nameof(callback)); - /// - /// Runs the specified asynchronous method to completion while synchronously blocking the calling thread. - /// - /// The asynchronous method to execute. - /// - /// Any exception thrown by the delegate is rethrown in its original type to the caller of this method. - /// When the delegate resumes from a yielding await, the default behavior is to resume in its original context - /// as an ordinary async method execution would. For example, if the caller was on the main thread, execution - /// resumes after an await on the main thread; but if it started on a threadpool thread it resumes on a threadpool thread. - /// - /// - /// // On threadpool or Main thread, this method will block - /// // the calling thread until all async operations in the - /// // delegate complete. - /// joinableTaskFactory.Run(async delegate { - /// // still on the threadpool or Main thread as before. - /// await OperationAsync(); - /// // still on the threadpool or Main thread as before. - /// await Task.Run(async delegate { - /// // Now we're on a threadpool thread. - /// await Task.Yield(); - /// // still on a threadpool thread. - /// }); - /// // Now back on the Main thread (or threadpool thread if that's where we started). - /// }); - /// - /// - /// - public void Run(Func asyncMethod) + if (this.UnderlyingSynchronizationContext is object) { - this.Run(asyncMethod, JoinableTaskCreationOptions.None, entrypointOverride: null); + this.PostToUnderlyingSynchronizationContext(SingleExecuteProtector.ExecuteOnce, callback); } - - /// - /// Runs the specified asynchronous method to completion while synchronously blocking the calling thread. - /// - /// The asynchronous method to execute. - /// The used to customize the task's behavior. - public void Run(Func asyncMethod, JoinableTaskCreationOptions creationOptions) + else { - this.Run(asyncMethod, creationOptions, entrypointOverride: null); + ThreadPool.QueueUserWorkItem(SingleExecuteProtector.ExecuteOnceWaitCallback, callback); } + } - /// - /// Runs the specified asynchronous method to completion while synchronously blocking the calling thread. - /// - /// The type of value returned by the asynchronous operation. - /// The asynchronous method to execute. - /// The result of the Task returned by . - /// - /// Any exception thrown by the delegate is rethrown in its original type to the caller of this method. - /// When the delegate resumes from a yielding await, the default behavior is to resume in its original context - /// as an ordinary async method execution would. For example, if the caller was on the main thread, execution - /// resumes after an await on the main thread; but if it started on a threadpool thread it resumes on a threadpool thread. - /// See the overload documentation for an example. - /// - public T Run(Func> asyncMethod) - { - return this.Run(asyncMethod, JoinableTaskCreationOptions.None); - } + /// Runs the specified asynchronous method. + /// The asynchronous method to execute. + /// The used to customize the task's behavior. + /// The delegate to record as the entrypoint for this JoinableTask. + internal void Run(Func asyncMethod, JoinableTaskCreationOptions creationOptions, Delegate? entrypointOverride) + { + VerifyNoNonConcurrentSyncContext(); + JoinableTask? joinable = this.RunAsync(asyncMethod, synchronouslyBlocking: true, parentToken: null, creationOptions, entrypointOverride); + joinable.CompleteOnCurrentThread(); + } - /// - /// Runs the specified asynchronous method to completion while synchronously blocking the calling thread. - /// - /// The type of value returned by the asynchronous operation. - /// The asynchronous method to execute. - /// The used to customize the task's behavior. - /// The result of the Task returned by . - /// - /// Any exception thrown by the delegate is rethrown in its original type to the caller of this method. - /// When the delegate resumes from a yielding await, the default behavior is to resume in its original context - /// as an ordinary async method execution would. For example, if the caller was on the main thread, execution - /// resumes after an await on the main thread; but if it started on a threadpool thread it resumes on a threadpool thread. - /// - public T Run(Func> asyncMethod, JoinableTaskCreationOptions creationOptions) - { - VerifyNoNonConcurrentSyncContext(); - JoinableTask? joinable = this.RunAsync(asyncMethod, synchronouslyBlocking: true, creationOptions: creationOptions); - return joinable.CompleteOnCurrentThread(); - } + internal void Post(SendOrPostCallback callback, object? state, bool mainThreadAffinitized) + { + Requires.NotNull(callback, nameof(callback)); - /// - /// Invokes an async delegate on the caller's thread, and yields back to the caller when the async method yields. - /// The async delegate is invoked in such a way as to mitigate deadlocks in the event that the async method - /// requires the main thread while the main thread is blocked waiting for the async method's completion. - /// - /// The method that, when executed, will begin the async operation. - /// An object that tracks the completion of the async operation, and allows for later synchronous blocking of the main thread for completion if necessary. - /// - /// Exceptions thrown by the delegate are captured by the returned . - /// When the delegate resumes from a yielding await, the default behavior is to resume in its original context - /// as an ordinary async method execution would. For example, if the caller was on the main thread, execution - /// resumes after an await on the main thread; but if it started on a threadpool thread it resumes on a threadpool thread. - /// - public JoinableTask RunAsync(Func asyncMethod) + if (mainThreadAffinitized) { - return this.RunAsync(asyncMethod, synchronouslyBlocking: false, creationOptions: JoinableTaskCreationOptions.None); - } + JoinableTask? transient = this.RunAsync(delegate + { + RoslynDebug.Assert(this.Context.AmbientTask is object, $"{nameof(this.Context.AmbientTask)} is always set for {nameof(this.RunAsync)} callbacks."); - /// - /// Invokes an async delegate on the caller's thread, and yields back to the caller when the async method yields. - /// The async delegate is invoked in such a way as to mitigate deadlocks in the event that the async method - /// requires the main thread while the main thread is blocked waiting for the async method's completion. - /// - /// The method that, when executed, will begin the async operation. - /// An object that tracks the completion of the async operation, and allows for later synchronous blocking of the main thread for completion if necessary. - /// The used to customize the task's behavior. - /// - /// Exceptions thrown by the delegate are captured by the returned . - /// When the delegate resumes from a yielding await, the default behavior is to resume in its original context - /// as an ordinary async method execution would. For example, if the caller was on the main thread, execution - /// resumes after an await on the main thread; but if it started on a threadpool thread it resumes on a threadpool thread. - /// - public JoinableTask RunAsync(Func asyncMethod, JoinableTaskCreationOptions creationOptions) - { - return this.RunAsync(asyncMethod, synchronouslyBlocking: false, creationOptions: creationOptions); - } + this.Context.AmbientTask.Post(callback, state, true); + return Task.CompletedTask; + }); - /// - /// Invokes an async delegate on the caller's thread, and yields back to the caller when the async method yields. - /// The async delegate is invoked in such a way as to mitigate deadlocks in the event that the async method - /// requires the main thread while the main thread is blocked waiting for the async method's completion. - /// - /// The type of value returned by the asynchronous operation. - /// The method that, when executed, will begin the async operation. - /// - /// An object that tracks the completion of the async operation, and allows for later synchronous blocking of the main thread for completion if necessary. - /// - /// - /// Exceptions thrown by the delegate are captured by the returned . - /// When the delegate resumes from a yielding await, the default behavior is to resume in its original context - /// as an ordinary async method execution would. For example, if the caller was on the main thread, execution - /// resumes after an await on the main thread; but if it started on a threadpool thread it resumes on a threadpool thread. - /// - public JoinableTask RunAsync(Func> asyncMethod) - { - return this.RunAsync(asyncMethod, synchronouslyBlocking: false, creationOptions: JoinableTaskCreationOptions.None); + if (transient.Task.IsFaulted) + { + // rethrow the exception. + transient.Task.GetAwaiter().GetResult(); + } } - - /// - /// Invokes an async delegate on the caller's thread, and yields back to the caller when the async method yields. - /// The async delegate is invoked in such a way as to mitigate deadlocks in the event that the async method - /// requires the main thread while the main thread is blocked waiting for the async method's completion. - /// - /// The type of value returned by the asynchronous operation. - /// The method that, when executed, will begin the async operation. - /// The used to customize the task's behavior. - /// - /// An object that tracks the completion of the async operation, and allows for later synchronous blocking of the main thread for completion if necessary. - /// - /// - /// Exceptions thrown by the delegate are captured by the returned . - /// When the delegate resumes from a yielding await, the default behavior is to resume in its original context - /// as an ordinary async method execution would. For example, if the caller was on the main thread, execution - /// resumes after an await on the main thread; but if it started on a threadpool thread it resumes on a threadpool thread. - /// - public JoinableTask RunAsync(Func> asyncMethod, JoinableTaskCreationOptions creationOptions) + else { - return this.RunAsync(asyncMethod, synchronouslyBlocking: false, creationOptions: creationOptions); + ThreadPool.QueueUserWorkItem(new WaitCallback(callback), state); } + } - /// - /// Responds to calls to - /// by scheduling a continuation to execute on the Main thread. - /// - /// The callback to invoke. - internal SingleExecuteProtector RequestSwitchToMainThread(Action callback) - { - Requires.NotNull(callback, nameof(callback)); - - // Make sure that this thread switch request is in a job that is captured by the job collection - // to which this switch request belongs. - // If an ambient job already exists and belongs to the collection, that's good enough. But if - // there is no ambient job, or the ambient job does not belong to the collection, we must create - // a (child) job and add that to this job factory's collection so that folks joining that factory - // can help this switch to complete. - JoinableTask? ambientJob = this.Context.AmbientTask; - SingleExecuteProtector? wrapper = null; - if (ambientJob is null || (this.jobCollection is object && !this.jobCollection.Contains(ambientJob))) - { - JoinableTask? transient = this.RunAsync( - delegate - { - RoslynDebug.Assert(this.Context.AmbientTask is object, $"{nameof(this.Context.AmbientTask)} is always set for {nameof(this.RunAsync)} callbacks."); - - ambientJob = this.Context.AmbientTask; - wrapper = SingleExecuteProtector.Create(ambientJob, callback); - ambientJob.Post(SingleExecuteProtector.ExecuteOnce, wrapper, true); - return Task.CompletedTask; - }, - synchronouslyBlocking: false, - creationOptions: JoinableTaskCreationOptions.None, - entrypointOverride: callback); - - if (transient.Task.IsFaulted) - { - // rethrow the exception. - transient.Task.GetAwaiter().GetResult(); - } - } - else - { - wrapper = SingleExecuteProtector.Create(ambientJob, callback); - ambientJob.Post(SingleExecuteProtector.ExecuteOnce, wrapper, true); - } + /// + /// Posts a message to the specified underlying SynchronizationContext for processing when the main thread + /// is freely available. + /// + /// The callback to invoke. + /// State to pass to the callback. + protected internal virtual void PostToUnderlyingSynchronizationContext(SendOrPostCallback callback, object state) + { + Requires.NotNull(callback, nameof(callback)); + Assumes.NotNull(this.UnderlyingSynchronizationContext); - Assumes.NotNull(wrapper); - return wrapper; - } + this.UnderlyingSynchronizationContext.Post(callback, state); + } - /// - /// Posts a callback to the main thread via the underlying dispatcher, - /// or to the threadpool when no dispatcher exists on the main thread. - /// - internal void PostToUnderlyingSynchronizationContextOrThreadPool(SingleExecuteProtector callback) - { - Requires.NotNull(callback, nameof(callback)); + /// + /// Raised when a joinable task has requested a transition to the main thread. + /// + /// The task requesting the transition to the main thread. + /// + /// This event may be raised on any thread, including the main thread. + /// + protected internal virtual void OnTransitioningToMainThread(JoinableTask joinableTask) + { + Requires.NotNull(joinableTask, nameof(joinableTask)); + } - if (this.UnderlyingSynchronizationContext is object) - { - this.PostToUnderlyingSynchronizationContext(SingleExecuteProtector.ExecuteOnce, callback); - } - else + /// + /// Raised whenever a joinable task has completed a transition to the main thread. + /// + /// The task whose request to transition to the main thread has completed. + /// A value indicating whether the transition was cancelled before it was fulfilled. + /// + /// This event is usually raised on the main thread, but can be on another thread when is . + /// + protected internal virtual void OnTransitionedToMainThread(JoinableTask joinableTask, bool canceled) + { + Requires.NotNull(joinableTask, nameof(joinableTask)); + } + + /// + /// Synchronously blocks the calling thread for the completion of the specified task. + /// If running on the main thread, any applicable message pump is suppressed + /// while the thread sleeps. + /// + /// The task whose completion is being waited on. + /// + /// Implementations should take care that exceptions from faulted or canceled tasks + /// not be thrown back to the caller. + /// + protected internal virtual void WaitSynchronously(Task task) + { + if (this.Context.IsOnMainThread) + { + // Suppress any reentrancy by causing this synchronously blocking wait + // to not pump any messages at all. + using (this.Context.NoMessagePumpSynchronizationContext.Apply()) { - ThreadPool.QueueUserWorkItem(SingleExecuteProtector.ExecuteOnceWaitCallback, callback); + this.WaitSynchronouslyCore(task); } } - - /// Runs the specified asynchronous method. - /// The asynchronous method to execute. - /// The used to customize the task's behavior. - /// The delegate to record as the entrypoint for this JoinableTask. - internal void Run(Func asyncMethod, JoinableTaskCreationOptions creationOptions, Delegate? entrypointOverride) + else { - VerifyNoNonConcurrentSyncContext(); - JoinableTask? joinable = this.RunAsync(asyncMethod, synchronouslyBlocking: true, creationOptions: creationOptions, entrypointOverride: entrypointOverride); - joinable.CompleteOnCurrentThread(); + this.WaitSynchronouslyCore(task); } + } - internal void Post(SendOrPostCallback callback, object? state, bool mainThreadAffinitized) + /// + /// Synchronously blocks the calling thread for the completion of the specified task. + /// + /// The task whose completion is being waited on. + /// + /// Implementations should take care that exceptions from faulted or canceled tasks + /// not be thrown back to the caller. + /// + protected virtual void WaitSynchronouslyCore(Task task) + { + Requires.NotNull(task, nameof(task)); + + if (this.Context.IsOnMainThread) { - Requires.NotNull(callback, nameof(callback)); + this.Context.IncrementMainThreadBlockingCount(); + } - if (mainThreadAffinitized) + int hangTimeoutsCount = 0; // useful for debugging dump files to see how many times we looped. + int hangNotificationCount = 0; + Guid hangId = Guid.Empty; + Stopwatch? stopWatch = null; + try + { + while (!task.Wait(this.HangDetectionTimeout)) { - JoinableTask? transient = this.RunAsync(delegate + if (hangTimeoutsCount == 0) { - RoslynDebug.Assert(this.Context.AmbientTask is object, $"{nameof(this.Context.AmbientTask)} is always set for {nameof(this.RunAsync)} callbacks."); + stopWatch = Stopwatch.StartNew(); + } - this.Context.AmbientTask.Post(callback, state, true); - return Task.CompletedTask; - }); + hangTimeoutsCount++; + TimeSpan hangDuration = TimeSpan.FromMilliseconds(this.HangDetectionTimeout.TotalMilliseconds * hangTimeoutsCount); + if (hangId == Guid.Empty) + { + hangId = Guid.NewGuid(); + } - if (transient.Task.IsFaulted) + if (!this.IsWaitingOnLongRunningTask()) { - // rethrow the exception. - transient.Task.GetAwaiter().GetResult(); + hangNotificationCount++; + this.Context.OnHangDetected(hangDuration, hangNotificationCount, hangId); } } - else + + if (hangNotificationCount > 0) { - ThreadPool.QueueUserWorkItem(new WaitCallback(callback), state); + RoslynDebug.Assert(stopWatch is object); + + // We detect a false alarm. The stop watch was started after the first timeout, so we add intial timeout to the total delay. + this.Context.OnFalseHangDetected( + stopWatch.Elapsed + this.HangDetectionTimeout, + hangId); } } - - /// - /// Posts a message to the specified underlying SynchronizationContext for processing when the main thread - /// is freely available. - /// - /// The callback to invoke. - /// State to pass to the callback. - protected internal virtual void PostToUnderlyingSynchronizationContext(SendOrPostCallback callback, object state) + catch (AggregateException) { - Requires.NotNull(callback, nameof(callback)); - Assumes.NotNull(this.UnderlyingSynchronizationContext); - - this.UnderlyingSynchronizationContext.Post(callback, state); + // Swallow exceptions thrown by Task.Wait(). + // Our caller just wants to know when the Task completes, + // whether successfully or not. } - - /// - /// Raised when a joinable task has requested a transition to the main thread. - /// - /// The task requesting the transition to the main thread. - /// - /// This event may be raised on any thread, including the main thread. - /// - protected internal virtual void OnTransitioningToMainThread(JoinableTask joinableTask) + finally { - Requires.NotNull(joinableTask, nameof(joinableTask)); + if (this.Context.IsOnMainThread) + { + this.Context.DecrementMainThreadBlockingCount(); + } } + } - /// - /// Raised whenever a joinable task has completed a transition to the main thread. - /// - /// The task whose request to transition to the main thread has completed. - /// A value indicating whether the transition was cancelled before it was fulfilled. - /// - /// This event is usually raised on the main thread, but can be on another thread when is true. - /// - protected internal virtual void OnTransitionedToMainThread(JoinableTask joinableTask, bool canceled) + /// + /// Check whether the current joinableTask is waiting on a long running task. + /// + /// Return true if the current synchronous task on the thread is waiting on a long running task. + protected bool IsWaitingOnLongRunningTask() + { + JoinableTask? currentBlockingTask = JoinableTask.TaskCompletingOnThisThread; + if (currentBlockingTask is object) { - Requires.NotNull(joinableTask, nameof(joinableTask)); - } + if ((currentBlockingTask.CreationOptions & JoinableTaskCreationOptions.LongRunning) == JoinableTaskCreationOptions.LongRunning) + { + return true; + } - /// - /// Synchronously blocks the calling thread for the completion of the specified task. - /// If running on the main thread, any applicable message pump is suppressed - /// while the thread sleeps. - /// - /// The task whose completion is being waited on. - /// - /// Implementations should take care that exceptions from faulted or canceled tasks - /// not be thrown back to the caller. - /// - protected internal virtual void WaitSynchronously(Task task) - { - if (this.Context.IsOnMainThread) + using (this.Context.NoMessagePumpSynchronizationContext.Apply()) { - // Suppress any reentrancy by causing this synchronously blocking wait - // to not pump any messages at all. - using (this.Context.NoMessagePumpSynchronizationContext.Apply()) + var allJoinedJobs = new HashSet(); + lock (this.Context.SyncContextLock) { - this.WaitSynchronouslyCore(task); + JoinableTaskDependencyGraph.AddSelfAndDescendentOrJoinedJobs(currentBlockingTask, allJoinedJobs); + return allJoinedJobs.Any(t => (t.CreationOptions & JoinableTaskCreationOptions.LongRunning) == JoinableTaskCreationOptions.LongRunning); } } - else - { - this.WaitSynchronouslyCore(task); - } } - /// - /// Synchronously blocks the calling thread for the completion of the specified task. - /// - /// The task whose completion is being waited on. - /// - /// Implementations should take care that exceptions from faulted or canceled tasks - /// not be thrown back to the caller. - /// - protected virtual void WaitSynchronouslyCore(Task task) + return false; + } + + /// + /// Adds the specified joinable task to the applicable collection. + /// + protected void Add(JoinableTask joinable) + { + Requires.NotNull(joinable, nameof(joinable)); + if (this.jobCollection is object) { - Requires.NotNull(task, nameof(task)); - int hangTimeoutsCount = 0; // useful for debugging dump files to see how many times we looped. - int hangNotificationCount = 0; - Guid hangId = Guid.Empty; - Stopwatch? stopWatch = null; - try - { - while (!task.Wait(this.HangDetectionTimeout)) - { - if (hangTimeoutsCount == 0) - { - stopWatch = Stopwatch.StartNew(); - } + this.jobCollection.Add(joinable); + } + } - hangTimeoutsCount++; - TimeSpan hangDuration = TimeSpan.FromMilliseconds(this.HangDetectionTimeout.TotalMilliseconds * hangTimeoutsCount); - if (hangId == Guid.Empty) - { - hangId = Guid.NewGuid(); - } + /// + /// Throws an exception if an active AsyncReaderWriterLock + /// upgradeable read or write lock is held by the caller. + /// + /// + /// This is important to call from the Run and Run{T} methods because + /// if they are called from within an ARWL upgradeable read or write lock, + /// then Run will synchronously block while inside the semaphore held + /// by the ARWL that prevents concurrency. If the delegate within Run + /// yields and then tries to reacquire the ARWL lock, it will be unable + /// to re-enter the semaphore, leading to a deadlock. + /// Instead, callers who hold UR/W locks should never call Run, or should + /// switch to the STA thread first in order to exit the semaphore before + /// calling the Run method. + /// + private static void VerifyNoNonConcurrentSyncContext() + { + // Don't use Verify.Operation here to avoid loading a string resource in success cases. + if (SynchronizationContext.Current is AsyncReaderWriterLock.NonConcurrentSynchronizationContext) + { + Report.Fail(Strings.NotAllowedUnderURorWLock); // pops a CHK assert dialog, but doesn't throw. + Verify.FailOperation(Strings.NotAllowedUnderURorWLock); // actually throws, even in RET. + } + } - if (!this.IsWaitingOnLongRunningTask()) - { - hangNotificationCount++; - this.Context.OnHangDetected(hangDuration, hangNotificationCount, hangId); - } - } + /// + /// Wraps the invocation of an async method such that it may + /// execute asynchronously, but may potentially be + /// synchronously completed (waited on) in the future. + /// + /// The asynchronous method to execute. + /// + /// + /// + /// The entry method's info for diagnostics. + private JoinableTask RunAsync(Func asyncMethod, bool synchronouslyBlocking, string? parentToken, JoinableTaskCreationOptions creationOptions, Delegate? entrypointOverride = null) + { + Requires.NotNull(asyncMethod, nameof(asyncMethod)); - if (hangNotificationCount > 0) - { - RoslynDebug.Assert(stopWatch is object); + var job = new JoinableTask(this, synchronouslyBlocking, parentToken, creationOptions, entrypointOverride ?? asyncMethod); + this.ExecuteJob(asyncMethod, job); + return job; + } - // We detect a false alarm. The stop watch was started after the first timeout, so we add intial timeout to the total delay. - this.Context.OnFalseHangDetected( - stopWatch.Elapsed + this.HangDetectionTimeout, - hangId); - } - } - catch (AggregateException) - { - // Swallow exceptions thrown by Task.Wait(). - // Our caller just wants to know when the Task completes, - // whether successfully or not. - } - } + /// + /// Invokes an async delegate on the caller's thread, and yields back to the caller when the async method yields. + /// The async delegate is invoked in such a way as to mitigate deadlocks in the event that the async method + /// requires the main thread while the main thread is blocked waiting for the async method's completion. + /// + /// The type of value returned by the asynchronous operation. + /// The method that, when executed, will begin the async operation. + /// A value indicating whether the caller is synchronously blocking. + /// An optional token that identifies one or more instances, typically in other processes, that serve as 'parents' to this one. + /// The used to customize the task's behavior. + /// + /// An object that tracks the completion of the async operation, and allows for later synchronous blocking of the main thread for completion if necessary. + /// + /// + /// Exceptions thrown by the delegate are captured by the returned . + /// When the delegate resumes from a yielding await, the default behavior is to resume in its original context + /// as an ordinary async method execution would. For example, if the caller was on the main thread, execution + /// resumes after an await on the main thread; but if it started on a threadpool thread it resumes on a threadpool thread. + /// + private JoinableTask RunAsync(Func> asyncMethod, bool synchronouslyBlocking, string? parentToken, JoinableTaskCreationOptions creationOptions) + { + Requires.NotNull(asyncMethod, nameof(asyncMethod)); - /// - /// Check whether the current joinableTask is waiting on a long running task. - /// - /// Return true if the current synchronous task on the thread is waiting on a long running task. - protected bool IsWaitingOnLongRunningTask() + var job = new JoinableTask(this, synchronouslyBlocking, parentToken, creationOptions, asyncMethod); + this.ExecuteJob(asyncMethod, job); + return job; + } + + private void ExecuteJob(Func asyncMethod, JoinableTask job) + { + try { - JoinableTask? currentBlockingTask = JoinableTask.TaskCompletingOnThisThread; - if (currentBlockingTask is object) + using (var framework = new RunFramework(this, job)) { - if ((currentBlockingTask.CreationOptions & JoinableTaskCreationOptions.LongRunning) == JoinableTaskCreationOptions.LongRunning) + Task asyncMethodResult; + try { - return true; + asyncMethodResult = asyncMethod(); } - - using (this.Context.NoMessagePumpSynchronizationContext.Apply()) + catch (Exception ex) { - var allJoinedJobs = new HashSet(); - lock (this.Context.SyncContextLock) - { - JoinableTaskDependencyGraph.AddSelfAndDescendentOrJoinedJobs(currentBlockingTask, allJoinedJobs); - return allJoinedJobs.Any(t => (t.CreationOptions & JoinableTaskCreationOptions.LongRunning) == JoinableTaskCreationOptions.LongRunning); - } + var tcs = new TaskCompletionSource(); + tcs.SetException(ex); + asyncMethodResult = tcs.Task; } + + job.SetWrappedTask(asyncMethodResult); } + } + catch (Exception ex) when (FailFast(ex)) + { + // We use a crashing exception filter to capture all the detail possible (even before unwinding the callstack) + // when an exception is thrown from this critical method. + // In particular, we have seen the WeakReference object that is instantiated by "new RunFramework" throw OutOfMemoryException. + throw Assumes.NotReachable(); + } - return false; + static bool FailFast(Exception ex) + { + Environment.FailFast("Unexpected exception thrown in critical scheduling code.", ex); + throw Assumes.NotReachable(); } + } + + /// + /// A struct whose disposal will revert the effect of an earlier call to . + /// + public struct ProcessingDisabledOperation : IDisposable + { + private JoinableTask? owner; /// - /// Adds the specified joinable task to the applicable collection. + /// Initializes a new instance of the struct. /// - protected void Add(JoinableTask joinable) + /// The owner of this struct. + internal ProcessingDisabledOperation(JoinableTask owner) { - Requires.NotNull(joinable, nameof(joinable)); - if (this.jobCollection is object) - { - this.jobCollection.Add(joinable); - } + owner.DisableProcessing++; + this.owner = owner; } - /// - /// Throws an exception if an active AsyncReaderWriterLock - /// upgradeable read or write lock is held by the caller. - /// - /// - /// This is important to call from the Run and Run{T} methods because - /// if they are called from within an ARWL upgradeable read or write lock, - /// then Run will synchronously block while inside the semaphore held - /// by the ARWL that prevents concurrency. If the delegate within Run - /// yields and then tries to reacquire the ARWL lock, it will be unable - /// to re-enter the semaphore, leading to a deadlock. - /// Instead, callers who hold UR/W locks should never call Run, or should - /// switch to the STA thread first in order to exit the semaphore before - /// calling the Run method. - /// - private static void VerifyNoNonConcurrentSyncContext() + /// + public void Dispose() { - // Don't use Verify.Operation here to avoid loading a string resource in success cases. - if (SynchronizationContext.Current is AsyncReaderWriterLock.NonConcurrentSynchronizationContext) + if (this.owner is { } owner) { -#if NETFRAMEWORK || NETCOREAPP // Assertion failures crash on .NET Core < 3.0 - Report.Fail(Strings.NotAllowedUnderURorWLock); // pops a CHK assert dialog, but doesn't throw. -#endif - Verify.FailOperation(Strings.NotAllowedUnderURorWLock); // actually throws, even in RET. + owner.DisableProcessing--; + this.owner = null; } } + } - /// - /// Wraps the invocation of an async method such that it may - /// execute asynchronously, but may potentially be - /// synchronously completed (waited on) in the future. - /// - /// The asynchronous method to execute. - /// A value indicating whether the launching thread will synchronously block for this job's completion. - /// The used to customize the task's behavior. - /// The entry method's info for diagnostics. - private JoinableTask RunAsync(Func asyncMethod, bool synchronouslyBlocking, JoinableTaskCreationOptions creationOptions, Delegate? entrypointOverride = null) - { - Requires.NotNull(asyncMethod, nameof(asyncMethod)); - - var job = new JoinableTask(this, synchronouslyBlocking, creationOptions, entrypointOverride ?? asyncMethod); - this.ExecuteJob(asyncMethod, job); - return job; - } + /// + /// An awaitable struct that facilitates an asynchronous transition to the Main thread. + /// + public readonly struct MainThreadAwaitable + { + private readonly JoinableTaskFactory? jobFactory; - private JoinableTask RunAsync(Func> asyncMethod, bool synchronouslyBlocking, JoinableTaskCreationOptions creationOptions) - { - Requires.NotNull(asyncMethod, nameof(asyncMethod)); + private readonly JoinableTask? job; - var job = new JoinableTask(this, synchronouslyBlocking, creationOptions, asyncMethod); - this.ExecuteJob(asyncMethod, job); - return job; - } + private readonly CancellationToken cancellationToken; - private void ExecuteJob(Func asyncMethod, JoinableTask job) - { - try - { - using (var framework = new RunFramework(this, job)) - { - Task asyncMethodResult; - try - { - asyncMethodResult = asyncMethod(); - } - catch (Exception ex) - { - var tcs = new TaskCompletionSource(); - tcs.SetException(ex); - asyncMethodResult = tcs.Task; - } + private readonly bool alwaysYield; - job.SetWrappedTask(asyncMethodResult); - } - } - catch (Exception ex) when (FailFast(ex)) - { - // We use a crashing exception filter to capture all the detail possible (even before unwinding the callstack) - // when an exception is thrown from this critical method. - // In particular, we have seen the WeakReference object that is instantiated by "new RunFramework" throw OutOfMemoryException. - throw Assumes.NotReachable(); - } + private readonly bool throwOnCancellation; - static bool FailFast(Exception ex) - { - Environment.FailFast("Unexpected exception thrown in critical scheduling code.", ex); - throw Assumes.NotReachable(); - } + /// + /// Initializes a new instance of the struct. + /// + internal MainThreadAwaitable(JoinableTaskFactory jobFactory, JoinableTask? job, CancellationToken cancellationToken, bool alwaysYield = false) + : this(jobFactory, job, alwaysYield, throwOnCancellation: true, cancellationToken) + { } /// - /// An awaitable struct that facilitates an asynchronous transition to the Main thread. + /// Initializes a new instance of the struct. /// - public readonly struct MainThreadAwaitable + private MainThreadAwaitable(JoinableTaskFactory jobFactory, JoinableTask? job, bool alwaysYield, bool throwOnCancellation, CancellationToken cancellationToken) { - private readonly JoinableTaskFactory? jobFactory; - - private readonly JoinableTask? job; + Requires.NotNull(jobFactory, nameof(jobFactory)); - private readonly CancellationToken cancellationToken; - - private readonly bool alwaysYield; + this.jobFactory = jobFactory; + this.job = job; + this.cancellationToken = cancellationToken; + this.alwaysYield = alwaysYield; + this.throwOnCancellation = throwOnCancellation; + } - /// - /// Initializes a new instance of the struct. - /// - internal MainThreadAwaitable(JoinableTaskFactory jobFactory, JoinableTask? job, CancellationToken cancellationToken, bool alwaysYield = false) + /// + /// Returns an awaitable for the specified + /// operation that will not throw an exception if cancellation is requested. + /// + /// An awaitable. + public MainThreadAwaitable NoThrowAwaitable() + { + if (this.jobFactory is null) { - Requires.NotNull(jobFactory, nameof(jobFactory)); - - this.jobFactory = jobFactory; - this.job = job; - this.cancellationToken = cancellationToken; - this.alwaysYield = alwaysYield; + return default; } - /// - /// Gets the awaiter. - /// - public MainThreadAwaiter GetAwaiter() - { - if (this.jobFactory is null) - { - return default; - } - - return new MainThreadAwaiter(this.jobFactory, this.job, this.alwaysYield, this.cancellationToken); - } + return new MainThreadAwaitable(this.jobFactory, this.job, this.alwaysYield, throwOnCancellation: false, this.cancellationToken); } /// - /// An awaiter struct that facilitates an asynchronous transition to the Main thread. + /// Gets the awaiter. /// - public readonly struct MainThreadAwaiter : ICriticalNotifyCompletion + public MainThreadAwaiter GetAwaiter() { - private static readonly Action SafeCancellationAction = state => ThreadPool.QueueUserWorkItem(SingleExecuteProtector.ExecuteOnceWaitCallback, state); + if (this.jobFactory is null) + { + return default; + } - private static readonly Action UnsafeCancellationAction = state => ThreadPool.UnsafeQueueUserWorkItem(SingleExecuteProtector.ExecuteOnceWaitCallback, state); + return new MainThreadAwaiter(this.jobFactory, this.job, this.alwaysYield, this.throwOnCancellation, this.cancellationToken); + } + } + + /// + /// An awaiter struct that facilitates an asynchronous transition to the Main thread. + /// + public readonly struct MainThreadAwaiter : ICriticalNotifyCompletion + { + private static readonly Action SafeCancellationAction = state => ThreadPool.QueueUserWorkItem(SingleExecuteProtector.ExecuteOnceWaitCallback, state); - private readonly JoinableTaskFactory? jobFactory; + private static readonly Action UnsafeCancellationAction = state => ThreadPool.UnsafeQueueUserWorkItem(SingleExecuteProtector.ExecuteOnceWaitCallback, state); - private readonly CancellationToken cancellationToken; + private readonly JoinableTaskFactory? jobFactory; - private readonly bool alwaysYield; + private readonly CancellationToken cancellationToken; - private readonly JoinableTask? job; + private readonly bool alwaysYield; - private readonly bool synchronousCancellation; + private readonly bool throwOnCancellation; - /// - /// Holds the reference to the struct, so that all the copies of will hold - /// the same object. - /// - /// - /// This must be initialized to either null or an object holding no value. - /// If this starts as an object object holding no value, then it means we are interested in the cancellation, - /// and its state would be changed following one of these 2 patterns determined by the execution order. - /// 1. if finishes before is being executed on main thread, - /// then this will hold the real registered value after , and - /// will dispose that value and set a default value of . - /// 2. if is executed on main thread before registers the cancellation, - /// then this will hold a default value of , and - /// would not touch it. - /// - private readonly StrongBox? cancellationRegistrationPtr; + private readonly JoinableTask? job; - /// - /// Initializes a new instance of the struct. - /// - internal MainThreadAwaiter(JoinableTaskFactory jobFactory, JoinableTask? job, bool alwaysYield, CancellationToken cancellationToken) - { - this.jobFactory = jobFactory; - this.job = job; - this.cancellationToken = cancellationToken; - this.synchronousCancellation = cancellationToken.IsCancellationRequested && !alwaysYield; - this.alwaysYield = alwaysYield; - - // Don't allocate the pointer if the cancellation token can't be canceled (or already is): - this.cancellationRegistrationPtr = cancellationToken.CanBeCanceled && !this.synchronousCancellation - ? new StrongBox() - : null; - } + private readonly bool synchronousCancellation; - /// - /// Gets a value indicating whether the caller is already on the Main thread. - /// - public bool IsCompleted + /// + /// Holds the reference to the struct, so that all the copies of will hold + /// the same object. + /// + /// + /// This must be initialized to either null or an object holding no value. + /// If this starts as an object object holding no value, then it means we are interested in the cancellation, + /// and its state would be changed following one of these 2 patterns determined by the execution order. + /// 1. if finishes before is being executed on main thread, + /// then this will hold the real registered value after , and + /// will dispose that value and set a default value of . + /// 2. if is executed on main thread before registers the cancellation, + /// then this will hold a default value of , and + /// would not touch it. + /// + private readonly StrongBox? cancellationRegistrationPtr; + + /// + /// Initializes a new instance of the struct. + /// + internal MainThreadAwaiter(JoinableTaskFactory jobFactory, JoinableTask? job, bool alwaysYield, bool throwOnCancellation, CancellationToken cancellationToken) + { + this.jobFactory = jobFactory; + this.job = job; + this.cancellationToken = cancellationToken; + this.synchronousCancellation = cancellationToken.IsCancellationRequested && !alwaysYield; + this.alwaysYield = alwaysYield; + this.throwOnCancellation = throwOnCancellation; + + // Don't allocate the pointer if the cancellation token can't be canceled (or already is): + this.cancellationRegistrationPtr = cancellationToken.CanBeCanceled && !this.synchronousCancellation + ? new StrongBox() + : null; + } + + /// + /// Gets a value indicating whether the caller is already on the Main thread. + /// + public bool IsCompleted + { + get { - get + if (this.alwaysYield) { - if (this.alwaysYield) - { - return false; - } - - return this.synchronousCancellation - || this.jobFactory is null - || this.jobFactory.Context.IsOnMainThread - || this.jobFactory.Context.UnderlyingSynchronizationContext is null; + return false; } - } - /// - /// Schedules a continuation for execution on the Main thread - /// without capturing the ExecutionContext. - /// - /// The action to invoke when the operation completes. - public void UnsafeOnCompleted(Action continuation) - { - this.OnCompleted(continuation, flowExecutionContext: false); + return this.synchronousCancellation + || this.jobFactory is null + || this.jobFactory.Context.IsOnMainThread + || this.jobFactory.Context.UnderlyingSynchronizationContext is null; } + } + + /// + /// Schedules a continuation for execution on the Main thread + /// without capturing the ExecutionContext. + /// + /// The action to invoke when the operation completes. + public void UnsafeOnCompleted(Action continuation) + { + this.OnCompleted(continuation, flowExecutionContext: false); + } + + /// + /// Schedules a continuation for execution on the Main thread. + /// + /// The action to invoke when the operation completes. + public void OnCompleted(Action continuation) + { + this.OnCompleted(continuation, flowExecutionContext: true); + } - /// - /// Schedules a continuation for execution on the Main thread. - /// - /// The action to invoke when the operation completes. - public void OnCompleted(Action continuation) + /// + /// Called on the Main thread to prepare it to execute the continuation. + /// + public void GetResult() + { + Assumes.True(this.jobFactory is object); + if (!(this.jobFactory.Context.IsOnMainThread || this.jobFactory.Context.UnderlyingSynchronizationContext is null || this.cancellationToken.IsCancellationRequested)) { - this.OnCompleted(continuation, flowExecutionContext: true); + throw new JoinableTaskContextException(Strings.SwitchToMainThreadFailedToReachExpectedThread); } - /// - /// Called on the Main thread to prepare it to execute the continuation. - /// - public void GetResult() + // Release memory associated with the cancellation request. + if (this.cancellationRegistrationPtr is object) { - Assumes.True(this.jobFactory is object); - if (!(this.jobFactory.Context.IsOnMainThread || this.jobFactory.Context.UnderlyingSynchronizationContext is null || this.cancellationToken.IsCancellationRequested)) - { - throw new JoinableTaskContextException(Strings.SwitchToMainThreadFailedToReachExpectedThread); - } - - // Release memory associated with the cancellation request. - if (this.cancellationRegistrationPtr is object) + CancellationTokenRegistration registration = default(CancellationTokenRegistration); + using (this.jobFactory.Context.NoMessagePumpSynchronizationContext.Apply()) { - CancellationTokenRegistration registration = default(CancellationTokenRegistration); - using (this.jobFactory.Context.NoMessagePumpSynchronizationContext.Apply()) + lock (this.cancellationRegistrationPtr) { - lock (this.cancellationRegistrationPtr) + if (this.cancellationRegistrationPtr.Value.HasValue) { - if (this.cancellationRegistrationPtr.Value.HasValue) - { - registration = this.cancellationRegistrationPtr.Value.Value; - } - - // The reason we set this is to effectively null the struct that - // the strong box points to. Dispose does not seem to do this. If we - // have two copies of MainThreadAwaiter pointing to the same strongbox, - // then if one copy executes but the other does not, we could end - // up holding onto the memory pointed to through this pointer. By - // resetting the value here we make sure it gets cleaned. - // - // In addition, assigning default(CancellationTokenRegistration) to a field that - // stores a Nullable effectively gives it a HasValue status, - // which will let OnCompleted know it lost the interest on the cancellation. That is an - // important hint for OnCompleted() in order NOT to leak the cancellation registration. - this.cancellationRegistrationPtr.Value = default(CancellationTokenRegistration); + registration = this.cancellationRegistrationPtr.Value.Value; } - } - // Intentionally deferring disposal till we exit the lock to avoid executing outside code within the lock. - registration.Dispose(); + // The reason we set this is to effectively null the struct that + // the strong box points to. Dispose does not seem to do this. If we + // have two copies of MainThreadAwaiter pointing to the same strongbox, + // then if one copy executes but the other does not, we could end + // up holding onto the memory pointed to through this pointer. By + // resetting the value here we make sure it gets cleaned. + // + // In addition, assigning default(CancellationTokenRegistration) to a field that + // stores a Nullable effectively gives it a HasValue status, + // which will let OnCompleted know it lost the interest on the cancellation. That is an + // important hint for OnCompleted() in order NOT to leak the cancellation registration. + this.cancellationRegistrationPtr.Value = default(CancellationTokenRegistration); + } } - // If this method is called in a continuation after an actual yield, then SingleExecuteProtector.TryExecute - // should have already applied the appropriate SynchronizationContext to avoid deadlocks. - // However if no yield occurred then no TryExecute would have been invoked, so to avoid deadlocks in those - // cases, we apply the synchronization context here. - // We don't have an opportunity to revert the sync context change, but it turns out we don't need to because - // this method should only be called from async methods, which automatically revert any execution context - // changes they apply (including SynchronizationContext) when they complete, thanks to the way .NET 4.5 works. - SynchronizationContext? syncContext = this.job is object ? this.job.ApplicableJobSyncContext : this.jobFactory.ApplicableJobSyncContext; - syncContext.Apply(); + // Intentionally deferring disposal till we exit the lock to avoid executing outside code within the lock. + registration.Dispose(); + } + // If this method is called in a continuation after an actual yield, then SingleExecuteProtector.TryExecute + // should have already applied the appropriate SynchronizationContext to avoid deadlocks. + // However if no yield occurred then no TryExecute would have been invoked, so to avoid deadlocks in those + // cases, we apply the synchronization context here. + // We don't have an opportunity to revert the sync context change, but it turns out we don't need to because + // this method should only be called from async methods, which automatically revert any execution context + // changes they apply (including SynchronizationContext) when they complete, thanks to the way .NET 4.5 works. + SynchronizationContext? syncContext = this.job is object ? this.job.ApplicableJobSyncContext : this.jobFactory.ApplicableJobSyncContext; + syncContext.Apply(); + + if (this.throwOnCancellation) + { // Cancel if requested, even if we arrived on the main thread. // Unlike most async methods where throwing OperationCanceledException after completing the work may not be a good idea, // SwitchToMainThreadAsync is a scheduler method, and always precedes some work by the caller that almost certainly should // not be carried out if cancellation was requested. this.cancellationToken.ThrowIfCancellationRequested(); } + } - /// - /// Schedules a continuation for execution on the Main thread. - /// - /// The action to invoke when the operation completes. - /// A value indicating whether to capture and reapply the current ExecutionContext for the continuation. - private void OnCompleted(Action continuation, bool flowExecutionContext) + /// + /// Schedules a continuation for execution on the Main thread. + /// + /// The action to invoke when the operation completes. + /// A value indicating whether to capture and reapply the current ExecutionContext for the continuation. + private void OnCompleted(Action continuation, bool flowExecutionContext) + { + Assumes.True(this.jobFactory is object); + + bool restoreFlow = !flowExecutionContext && !ExecutionContext.IsFlowSuppressed(); + if (restoreFlow) { - Assumes.True(this.jobFactory is object); + ExecutionContext.SuppressFlow(); + } - bool restoreFlow = !flowExecutionContext && !ExecutionContext.IsFlowSuppressed(); - if (restoreFlow) - { - ExecutionContext.SuppressFlow(); - } + try + { + // In the event of a cancellation request, it becomes a race as to whether the threadpool + // or the main thread will execute the continuation first. So we must wrap the continuation + // in a SingleExecuteProtector so that it can't be executed twice by accident. + // Success case of the main thread. + SingleExecuteProtector? wrapper = this.jobFactory.RequestSwitchToMainThread(continuation); - try + // Cancellation case of a threadpool thread. + if (this.cancellationRegistrationPtr is object) { - // In the event of a cancellation request, it becomes a race as to whether the threadpool - // or the main thread will execute the continuation first. So we must wrap the continuation - // in a SingleExecuteProtector so that it can't be executed twice by accident. - // Success case of the main thread. - SingleExecuteProtector? wrapper = this.jobFactory.RequestSwitchToMainThread(continuation); - - // Cancellation case of a threadpool thread. - if (this.cancellationRegistrationPtr is object) + // Store the cancellation token registration in the struct pointer. This way, + // if the awaiter has been copied (since it's a struct), each copy of the awaiter + // points to the same registration. Without this we can have a memory leak. + CancellationTokenRegistration registration = this.cancellationToken.Register( + NullableHelpers.AsNullableArgAction(flowExecutionContext ? SafeCancellationAction : UnsafeCancellationAction), + wrapper, + useSynchronizationContext: false); + + // Needs a lock to avoid a race condition between this method and GetResult(). + // This method is usually called on a background thread. After "this.jobFactory.RequestSwitchToMainThread()" returns, + // the continuation is scheduled and GetResult() will be called whenever it is ready on main thread. + // We have observed sometimes GetResult() was called right after "this.jobFactory.RequestSwitchToMainThread()" + // and before "this.cancellationToken.Register()". If that happens, that means we lose the interest on the cancellation + // and should not register the cancellation here. Without protecting that, "this.cancellationRegistrationPtr" will be leaked. + bool disposeThisRegistration = false; + using (this.jobFactory.Context.NoMessagePumpSynchronizationContext.Apply()) { - // Store the cancellation token registration in the struct pointer. This way, - // if the awaiter has been copied (since it's a struct), each copy of the awaiter - // points to the same registration. Without this we can have a memory leak. - CancellationTokenRegistration registration = this.cancellationToken.Register( - NullableHelpers.AsNullableArgAction(flowExecutionContext ? SafeCancellationAction : UnsafeCancellationAction), - wrapper, - useSynchronizationContext: false); - - // Needs a lock to avoid a race condition between this method and GetResult(). - // This method is usually called on a background thread. After "this.jobFactory.RequestSwitchToMainThread()" returns, - // the continuation is scheduled and GetResult() will be called whenever it is ready on main thread. - // We have observed sometimes GetResult() was called right after "this.jobFactory.RequestSwitchToMainThread()" - // and before "this.cancellationToken.Register()". If that happens, that means we lose the interest on the cancellation - // and should not register the cancellation here. Without protecting that, "this.cancellationRegistrationPtr" will be leaked. - bool disposeThisRegistration = false; - using (this.jobFactory.Context.NoMessagePumpSynchronizationContext.Apply()) + lock (this.cancellationRegistrationPtr) { - lock (this.cancellationRegistrationPtr) + if (!this.cancellationRegistrationPtr.Value.HasValue) + { + this.cancellationRegistrationPtr.Value = registration; + } + else { - if (!this.cancellationRegistrationPtr.Value.HasValue) - { - this.cancellationRegistrationPtr.Value = registration; - } - else - { - disposeThisRegistration = true; - } + disposeThisRegistration = true; } } + } - if (disposeThisRegistration) - { - registration.Dispose(); - } + if (disposeThisRegistration) + { + registration.Dispose(); } } - catch (Exception ex) - { - // This is bad. It would cause a hang without a trace as to why, since if we can't - // schedule the continuation, stuff would just never happen. - // Crash now, so that a Watson report would capture the original error. - Environment.FailFast("Failed to schedule time on the UI thread. A continuation would never execute.", ex); - } - finally + } + catch (Exception ex) + { + // This is bad. It would cause a hang without a trace as to why, since if we can't + // schedule the continuation, stuff would just never happen. + // Crash now, so that a Watson report would capture the original error. + Environment.FailFast("Failed to schedule time on the UI thread. A continuation would never execute.", ex); + } + finally + { + if (restoreFlow) { - if (restoreFlow) - { - ExecutionContext.RestoreFlow(); - } + ExecutionContext.RestoreFlow(); } } } + } + + /// + /// A value to construct with a C# using block in all the Run method overloads + /// to setup and teardown the boilerplate stuff. + /// + private readonly struct RunFramework : IDisposable + { + private readonly JoinableTaskFactory factory; + private readonly SpecializedSyncContext syncContextRevert; + private readonly JoinableTask joinable; + private readonly JoinableTask? previousJoinable; /// - /// A value to construct with a C# using block in all the Run method overloads - /// to setup and teardown the boilerplate stuff. + /// Initializes a new instance of the struct + /// and sets up the synchronization contexts for the + /// family of methods. /// - private readonly struct RunFramework : IDisposable + internal RunFramework(JoinableTaskFactory factory, JoinableTask joinable) { - private readonly JoinableTaskFactory factory; - private readonly SpecializedSyncContext syncContextRevert; - private readonly JoinableTask joinable; - private readonly JoinableTask? previousJoinable; - - /// - /// Initializes a new instance of the struct - /// and sets up the synchronization contexts for the - /// family of methods. - /// - internal RunFramework(JoinableTaskFactory factory, JoinableTask joinable) + Requires.NotNull(factory, nameof(factory)); + Requires.NotNull(joinable, nameof(joinable)); + + this.factory = factory; + this.joinable = joinable; + this.factory.Add(joinable); + this.previousJoinable = this.factory.Context.AmbientTask; + this.factory.Context.AmbientTask = joinable; + this.syncContextRevert = this.joinable.ApplicableJobSyncContext.Apply(); + + // Join the ambient parent job, so the parent can dequeue this job's work. + if (this.previousJoinable is object && !this.previousJoinable.IsFullyCompleted) { - Requires.NotNull(factory, nameof(factory)); - Requires.NotNull(joinable, nameof(joinable)); - - this.factory = factory; - this.joinable = joinable; - this.factory.Add(joinable); - this.previousJoinable = this.factory.Context.AmbientTask; - this.factory.Context.AmbientTask = joinable; - this.syncContextRevert = this.joinable.ApplicableJobSyncContext.Apply(); - - // Join the ambient parent job, so the parent can dequeue this job's work. - if (this.previousJoinable is object && !this.previousJoinable.IsFullyCompleted) - { - JoinableTaskDependencyGraph.AddDependency(this.previousJoinable, joinable); + JoinableTaskDependencyGraph.AddDependency(this.previousJoinable, joinable); + if (!factory.Context.IsNoOpContext) + { // By definition we inherit the nesting factories of our immediate nesting task. ListOfOftenOne nestingFactories = this.previousJoinable.NestingFactories; @@ -1040,247 +1124,290 @@ internal RunFramework(JoinableTaskFactory factory, JoinableTask joinable) } } - /// - /// Reverts the execution context to its previous state before this struct was created. - /// - public void Dispose() + if (joinable.GetTokenizedParent() is JoinableTask tokenizedParent) { - this.syncContextRevert.Dispose(); - this.factory.Context.AmbientTask = this.previousJoinable; + JoinableTaskDependencyGraph.AddDependency(tokenizedParent, joinable); } } /// - /// A delegate wrapper that ensures the delegate is only invoked at most once. + /// Reverts the execution context to its previous state before this struct was created. + /// + public void Dispose() + { + this.syncContextRevert.Dispose(); + this.factory.Context.AmbientTask = this.previousJoinable; + } + } + + /// + /// A delegate wrapper that ensures the delegate is only invoked at most once. + /// + [DebuggerDisplay("{DelegateLabel}")] +#pragma warning disable VSOnly // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + internal class SingleExecuteProtector : IPendingExecutionRequestState +#pragma warning restore VSOnly // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + { + /// + /// Executes the delegate if it has not already executed. + /// + internal static readonly SendOrPostCallback ExecuteOnce = state => ((SingleExecuteProtector)state!).TryExecute(); + + /// + /// Executes the delegate if it has not already executed. + /// + internal static readonly WaitCallback ExecuteOnceWaitCallback = state => ((SingleExecuteProtector)state!).TryExecute(); + + /// + /// Tracks the next request ID to assign to for a new object. + /// + private static int nextRequestId; + + /// + /// The job that created this wrapper. + /// + private JoinableTask? job; + + /// + /// The ID to use when calling . + /// + private int? requestId; + + private bool raiseTransitionComplete; + + /// + /// The delegate to invoke. if it has already been invoked. + /// + /// May be of type or . + private object? invokeDelegate; + + /// + /// The value to pass to the delegate if it is a . + /// + private object? state; + + /// + /// Stores execution callbacks for . + /// + private ListOfOftenOne executingCallbacks; + + /// + /// Initializes a new instance of the class. + /// + private SingleExecuteProtector(JoinableTask job) + { + Requires.NotNull(job, nameof(job)); + this.job = job; + } + + /// + /// Gets a value indicating whether the current request has been completed, and can be skipped. + /// + bool IPendingExecutionRequestState.IsCompleted => this.HasBeenExecuted; + + /// + /// Gets a value indicating whether this instance has already executed. + /// + internal bool HasBeenExecuted + { + get { return this.invokeDelegate is null; } + } + + /// + /// Gets a string that describes the delegate that this instance invokes. + /// FOR DIAGNOSTIC PURPOSES ONLY. /// - [DebuggerDisplay("{DelegateLabel}")] - internal class SingleExecuteProtector + internal string DelegateLabel { - /// - /// Executes the delegate if it has not already executed. - /// - internal static readonly SendOrPostCallback ExecuteOnce = state => ((SingleExecuteProtector)state!).TryExecute(); - - /// - /// Executes the delegate if it has not already executed. - /// - internal static readonly WaitCallback ExecuteOnceWaitCallback = state => ((SingleExecuteProtector)state!).TryExecute(); - - /// - /// The job that created this wrapper. - /// - private JoinableTask? job; - - private bool raiseTransitionComplete; - - /// - /// The delegate to invoke. null if it has already been invoked. - /// - /// May be of type or . - private object? invokeDelegate; - - /// - /// The value to pass to the delegate if it is a . - /// - private object? state; - - /// - /// Stores execution callbacks for . - /// - private ListOfOftenOne executingCallbacks; - - /// - /// Initializes a new instance of the class. - /// - private SingleExecuteProtector(JoinableTask job) + [RequiresUnreferencedCode(Reasons.DiagnosticAnalysisOnly)] + get { - Requires.NotNull(job, nameof(job)); - this.job = job; + return this.WalkAsyncReturnStackFrames().First(); // Top frame of the return callstack. } + } + + /// + /// Returns a unique value for use when calling + /// and . + /// + /// The unique value. + /// + /// Values may be negative or zero if more than IDs have been created. + /// Values may be reused if more than 2^32 IDs have been created. + /// + internal static int GetNextRequestId() => Interlocked.Increment(ref nextRequestId); - /// - /// Gets a value indicating whether this instance has already executed. - /// - internal bool HasBeenExecuted + /// + /// Initializes a new instance of the class. + /// + /// The joinable task responsible for this work. + /// The delegate being wrapped. + /// An instance of . + internal static SingleExecuteProtector Create(JoinableTask job, Action action) + { + return new SingleExecuteProtector(job) { - get { return this.invokeDelegate is null; } - } + invokeDelegate = action, + }; + } + + /// + /// Initializes a new instance of the class + /// that describes the specified callback. + /// + /// The joinable task responsible for this work. + /// The callback to invoke. + /// The state object to pass to the callback. + /// An instance of . + internal static SingleExecuteProtector Create(JoinableTask job, SendOrPostCallback callback, object? state) + { + Requires.NotNull(job, nameof(job)); - /// - /// Gets a string that describes the delegate that this instance invokes. - /// FOR DIAGNOSTIC PURPOSES ONLY. - /// - internal string DelegateLabel + // As an optimization, recognize if what we're being handed is already an instance of this type, + // because if it is, we don't need to wrap it with yet another instance. + var existing = state as SingleExecuteProtector; + if (callback == ExecuteOnce && existing is object && job == existing.job) { - get - { - return this.WalkAsyncReturnStackFrames().First(); // Top frame of the return callstack. - } + return existing; } - /// - /// Initializes a new instance of the class. - /// - /// The joinable task responsible for this work. - /// The delegate being wrapped. - /// An instance of . - internal static SingleExecuteProtector Create(JoinableTask job, Action action) + return new SingleExecuteProtector(job) { - return new SingleExecuteProtector(job) - { - invokeDelegate = action, - }; - } + invokeDelegate = callback, + state = state, + }; + } - /// - /// Initializes a new instance of the class - /// that describes the specified callback. - /// - /// The joinable task responsible for this work. - /// The callback to invoke. - /// The state object to pass to the callback. - /// An instance of . - internal static SingleExecuteProtector Create(JoinableTask job, SendOrPostCallback callback, object? state) + /// + /// Registers for a callback when this instance is executed. + /// + internal void AddExecutingCallback(JoinableTask.ExecutionQueue callbackReceiver) + { + if (!this.HasBeenExecuted) { - Requires.NotNull(job, nameof(job)); + this.executingCallbacks.Add(callbackReceiver); + } + } - // As an optimization, recognize if what we're being handed is already an instance of this type, - // because if it is, we don't need to wrap it with yet another instance. - var existing = state as SingleExecuteProtector; - if (callback == ExecuteOnce && existing is object && job == existing.job) - { - return existing; - } + /// + /// Unregisters a callback for when this instance is executed. + /// + internal void RemoveExecutingCallback(JoinableTask.ExecutionQueue callbackReceiver) + { + this.executingCallbacks.Remove(callbackReceiver); + } - return new SingleExecuteProtector(job) - { - invokeDelegate = callback, - state = state, - }; + /// + /// Walk the continuation objects inside "async state machines" to generate the return callstack. + /// FOR DIAGNOSTIC PURPOSES ONLY. + /// + [RequiresUnreferencedCode(Reasons.DiagnosticAnalysisOnly)] + internal IEnumerable WalkAsyncReturnStackFrames() + { + // This instance might be a wrapper of another instance of "SingleExecuteProtector". + // If that is true, we need to follow the chain to find the inner instance of "SingleExecuteProtector". + SingleExecuteProtector? singleExecuteProtector = this; + while (singleExecuteProtector.state is SingleExecuteProtector) + { + singleExecuteProtector = (SingleExecuteProtector)singleExecuteProtector.state; } - /// - /// Registers for a callback when this instance is executed. - /// - internal void AddExecutingCallback(JoinableTask.ExecutionQueue callbackReceiver) + var invokeDelegate = singleExecuteProtector.invokeDelegate as Delegate; + var stateDelegate = singleExecuteProtector.state as Delegate; + + // We are in favor of "state" when "invokeDelegate" is a static method and "state" is the actual delegate. + Delegate? actualDelegate = (stateDelegate is object && stateDelegate.Target is object) ? stateDelegate : invokeDelegate; + if (actualDelegate is null) { - if (!this.HasBeenExecuted) - { - this.executingCallbacks.Add(callbackReceiver); - } + yield return ""; + yield break; } - /// - /// Unregisters a callback for when this instance is executed. - /// - internal void RemoveExecutingCallback(JoinableTask.ExecutionQueue callbackReceiver) + foreach (var frame in actualDelegate.GetAsyncReturnStackFrames()) { - this.executingCallbacks.Remove(callbackReceiver); + yield return frame; } + } - /// - /// Walk the continuation objects inside "async state machines" to generate the return callstack. - /// FOR DIAGNOSTIC PURPOSES ONLY. - /// - internal IEnumerable WalkAsyncReturnStackFrames() + internal void RaiseTransitioningEvents(bool mainThreadAffinitized, bool synchronouslyBlockingMainThread) + { + if (ThreadingEventSource.Instance.IsEnabled()) { - // This instance might be a wrapper of another instance of "SingleExecuteProtector". - // If that is true, we need to follow the chain to find the inner instance of "SingleExecuteProtector". - SingleExecuteProtector? singleExecuteProtector = this; - while (singleExecuteProtector.state is SingleExecuteProtector) - { - singleExecuteProtector = (SingleExecuteProtector)singleExecuteProtector.state; - } - - var invokeDelegate = singleExecuteProtector.invokeDelegate as Delegate; - var stateDelegate = singleExecuteProtector.state as Delegate; - - // We are in favor of "state" when "invokeDelegate" is a static method and "state" is the actual delegate. - Delegate? actualDelegate = (stateDelegate is object && stateDelegate.Target is object) ? stateDelegate : invokeDelegate; - if (actualDelegate is null) - { - yield return ""; - yield break; - } - - foreach (var frame in actualDelegate.GetAsyncReturnStackFrames()) - { - yield return frame; - } + this.requestId = GetNextRequestId(); + ThreadingEventSource.Instance.PostExecutionStart(this.requestId.Value, mainThreadAffinitized); } - internal void RaiseTransitioningEvents() + if (mainThreadAffinitized && !synchronouslyBlockingMainThread) { Assumes.False(this.raiseTransitionComplete); // if this method is called twice, that's the sign of a problem. - RoslynDebug.Assert(this.job is object); - this.raiseTransitionComplete = true; + RoslynDebug.Assert(this.job is object); this.job.Factory.OnTransitioningToMainThread(this.job); } + } - /// - /// Executes the delegate if it has not already executed. - /// - internal bool TryExecute() + /// + /// Executes the delegate if it has not already executed. + /// + internal bool TryExecute() + { + object? invokeDelegate = Interlocked.Exchange(ref this.invokeDelegate, null); + if (invokeDelegate is object) { - object? invokeDelegate = Interlocked.Exchange(ref this.invokeDelegate, null); - if (invokeDelegate is object) + this.OnExecuting(); + RoslynDebug.Assert(this.job is object); + SynchronizationContext? syncContext = this.job.ApplicableJobSyncContext; + using (syncContext.Apply(checkForChangesOnRevert: false)) { - this.OnExecuting(); - RoslynDebug.Assert(this.job is object); - SynchronizationContext? syncContext = this.job.ApplicableJobSyncContext; - using (syncContext.Apply(checkForChangesOnRevert: false)) + if (invokeDelegate is Action action) { - if (invokeDelegate is Action action) - { - action(); - } - else - { - var callback = (SendOrPostCallback)invokeDelegate; - callback(this.state); - } - - // Release the rest of the memory we're referencing. - this.state = null; - this.job = null; + action(); + } + else + { + var callback = (SendOrPostCallback)invokeDelegate; + callback(this.state); } - return true; - } - else - { - return false; + // Release the rest of the memory we're referencing. + this.state = null; + this.job = null; } + + return true; + } + else + { + return false; } + } - /// - /// Invokes handler. - /// - private void OnExecuting() + /// + /// Invokes handler. + /// + private void OnExecuting() + { + if (ThreadingEventSource.Instance.IsEnabled() && this.requestId is int requestId) { - if (ThreadingEventSource.Instance.IsEnabled()) - { - ThreadingEventSource.Instance.PostExecutionStop(this.GetHashCode()); - } + ThreadingEventSource.Instance.PostExecutionStop(requestId); + } - // While raising the event, automatically remove the handlers since we'll only - // raise them once, and we'd like to avoid holding references that may extend - // the lifetime of our recipients. - using (ListOfOftenOne.Enumerator enumerator = this.executingCallbacks.EnumerateAndClear()) + // While raising the event, automatically remove the handlers since we'll only + // raise them once, and we'd like to avoid holding references that may extend + // the lifetime of our recipients. + using (ListOfOftenOne.Enumerator enumerator = this.executingCallbacks.EnumerateAndClear()) + { + while (enumerator.MoveNext()) { - while (enumerator.MoveNext()) - { - enumerator.Current.OnExecuting(this, EventArgs.Empty); - } + enumerator.Current.OnExecuting(this, EventArgs.Empty); } + } - if (this.raiseTransitionComplete) - { - RoslynDebug.Assert(this.job is object); + if (this.raiseTransitionComplete) + { + RoslynDebug.Assert(this.job is object); - this.job.Factory.OnTransitionedToMainThread(this.job, !this.job.Factory.Context.IsOnMainThread); - } + this.job.Factory.OnTransitionedToMainThread(this.job, !this.job.Factory.Context.IsOnMainThread); } } } diff --git a/src/Microsoft.VisualStudio.Threading/JoinableTaskInternals.cs b/src/Microsoft.VisualStudio.Threading/JoinableTaskInternals.cs new file mode 100644 index 000000000..03577d2c5 --- /dev/null +++ b/src/Microsoft.VisualStudio.Threading/JoinableTaskInternals.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.ComponentModel; + +namespace Microsoft.VisualStudio.Threading; +#pragma warning disable RS0016 // Add public types and members to the declared API +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +/// +/// A helper class for integration with Visual Studio. +/// APIs in this file are intended for Microsoft internal use only +/// and are subject to change without notice. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class JoinableTaskInternals +{ + public static bool IsMainThreadBlockedByAnyJoinableTask(JoinableTaskContext? joinableTaskContext) + { + return joinableTaskContext?.IsMainThreadBlockedByAnyJoinableTask == true; + } + + public static JoinableTaskToken? GetJoinableTaskToken(JoinableTaskContext? joinableTaskContext) + { + if (joinableTaskContext?.AmbientTask?.WeakSelf is WeakReference currentTask) + { + return new JoinableTaskToken() { JoinableTaskReference = currentTask }; + } + + return null; + } + + public static bool IsMainThreadMaybeBlocked(JoinableTaskToken? joinableTaskToken) + { + if (joinableTaskToken?.JoinableTaskReference?.TryGetTarget(out JoinableTask? joinableTask) == true) + { + if (joinableTask is not null) + { + return joinableTask.MaybeBlockMainThread(); + } + } + + return false; + } + + public struct JoinableTaskToken + { + internal WeakReference? JoinableTaskReference; + } +} diff --git a/src/Microsoft.VisualStudio.Threading/JoinableTask`1.cs b/src/Microsoft.VisualStudio.Threading/JoinableTask`1.cs index 75e923df5..9affc39a1 100644 --- a/src/Microsoft.VisualStudio.Threading/JoinableTask`1.cs +++ b/src/Microsoft.VisualStudio.Threading/JoinableTask`1.cs @@ -1,18 +1,14 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + namespace Microsoft.VisualStudio.Threading { - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Linq; - using System.Reflection; - using System.Runtime.CompilerServices; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - /// /// Tracks asynchronous operations and provides the ability to Join those operations to avoid /// deadlocks while synchronously blocking the Main thread for the operation's completion. @@ -27,12 +23,13 @@ public class JoinableTask : JoinableTask /// /// Initializes a new instance of the class. /// - /// The instance that began the async operation. - /// A value indicating whether the launching thread will synchronously block for this job's completion. - /// The used to customize the task's behavior. - /// The entry method's info for diagnostics. - internal JoinableTask(JoinableTaskFactory owner, bool synchronouslyBlocking, JoinableTaskCreationOptions creationOptions, Delegate initialDelegate) - : base(owner, synchronouslyBlocking, creationOptions, initialDelegate) + /// + /// + /// + /// + /// + internal JoinableTask(JoinableTaskFactory owner, bool synchronouslyBlocking, string? parentToken, JoinableTaskCreationOptions creationOptions, Delegate initialDelegate) + : base(owner, synchronouslyBlocking, parentToken, creationOptions, initialDelegate) { } @@ -47,22 +44,13 @@ internal JoinableTask(JoinableTaskFactory owner, bool synchronouslyBlocking, Joi /// /// A cancellation token that will exit this method before the task is completed. /// A task that completes after the asynchronous operation completes and the join is reverted, with the result of the operation. + /// Thrown when is canceled. + /// + /// Any exception thrown by the asynchronous operation is propagated out to the caller of this method. + /// public new Task JoinAsync(CancellationToken cancellationToken = default(CancellationToken)) { - cancellationToken.ThrowIfCancellationRequested(); - if (this.IsCompleted) - { - Assumes.True(this.Task.IsCompleted); - return this.Task; - } - - return JoinSlowAsync(cancellationToken); - - async Task JoinSlowAsync(CancellationToken cancellationToken) - { - await base.JoinAsync(cancellationToken).ConfigureAwait(AwaitShouldCaptureSyncContext); - return await this.Task.ConfigureAwait(AwaitShouldCaptureSyncContext); - } + return this.JoinAsync(continueOnCapturedContext: AwaitShouldCaptureSyncContext, cancellationToken); } /// @@ -71,6 +59,10 @@ async Task JoinSlowAsync(CancellationToken cancellationToken) /// /// A cancellation token that will exit this method before the task is completed. /// The result of the asynchronous operation. + /// Thrown when is canceled. + /// + /// Any exception thrown by the asynchronous operation is propagated out to the caller of this method. + /// public new T Join(CancellationToken cancellationToken = default(CancellationToken)) { base.Join(cancellationToken); @@ -79,7 +71,7 @@ async Task JoinSlowAsync(CancellationToken cancellationToken) } /// - /// Gets an awaiter that is equivalent to calling . + /// Gets an awaiter that is equivalent to calling . /// /// A task whose result is the result of the asynchronous operation. public new TaskAwaiter GetAwaiter() @@ -93,11 +85,56 @@ async Task JoinSlowAsync(CancellationToken cancellationToken) return this.Task.GetAwaiter().GetResult(); } + /// + /// Joins any main thread affinity of the caller with the asynchronous operation to avoid deadlocks + /// in the event that the main thread ultimately synchronously blocks waiting for the operation to complete. + /// + /// A value indicating whether *internal* continuations required to respond to cancellation should run on the current . + /// A cancellation token that will exit this method before the task is completed. + /// A task that completes after the asynchronous operation completes and the join is reverted, with the result of the operation. + internal Task JoinAsync(bool continueOnCapturedContext, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + if (this.IsCompleted) + { + Assumes.True(this.Task.IsCompleted); + return this.Task; + } + + if (!cancellationToken.CanBeCanceled) + { + // A completed or failed JoinableTask will remove itself from parent dependency chains, so we don't repeat it which requires the sync lock. + _ = this.AmbientJobJoinsThis(); + return this.Task; + } + else + { + return JoinSlowAsync(this, continueOnCapturedContext, cancellationToken); + } + + static async Task JoinSlowAsync(JoinableTask me, bool continueOnCapturedContext, CancellationToken cancellationToken) + { + // No need to dispose of this except in cancellation case. + JoinableTaskCollection.JoinRelease dependency = me.AmbientJobJoinsThis(); + + try + { + await me.Task.WithCancellation(continueOnCapturedContext, cancellationToken).ConfigureAwait(continueOnCapturedContext); + return await me.Task.ConfigureAwait(continueOnCapturedContext); + } + catch (OperationCanceledException) + { + dependency.Dispose(); + throw; + } + } + } + /// - internal override object CreateTaskCompletionSource() => new TaskCompletionSourceWithoutInlining(allowInliningContinuations: false); + internal override object CreateTaskCompletionSource() => new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); /// - internal override Task GetTaskFromCompletionSource(object taskCompletionSource) => ((TaskCompletionSourceWithoutInlining)taskCompletionSource).Task; + internal override Task GetTaskFromCompletionSource(object taskCompletionSource) => ((TaskCompletionSource)taskCompletionSource).Task; /// internal override void CompleteTaskSourceFromWrappedTask(Task wrappedTask, object taskCompletionSource) => ((Task)wrappedTask).ApplyResultTo((TaskCompletionSource)taskCompletionSource); diff --git a/src/Microsoft.VisualStudio.Threading/LightUps.cs b/src/Microsoft.VisualStudio.Threading/LightUps.cs index cd9365310..71475c1d2 100644 --- a/src/Microsoft.VisualStudio.Threading/LightUps.cs +++ b/src/Microsoft.VisualStudio.Threading/LightUps.cs @@ -1,36 +1,35 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading -{ - using System; +using System; + +namespace Microsoft.VisualStudio.Threading; +/// +/// A non-generic class used to store statics that do not vary by generic type argument. +/// +internal static class LightUps +{ /// - /// A non-generic class used to store statics that do not vary by generic type argument. + /// Gets a value indicating whether we execute Windows 7 code even on later versions of Windows. /// - internal static class LightUps - { - /// - /// Gets a value indicating whether we execute Windows 7 code even on later versions of Windows. - /// - internal const bool ForceWindows7Mode = false; + internal const bool ForceWindows7Mode = false; - /// - /// The for Windows 8. - /// - private static readonly Version Windows8Version = new Version(6, 2, 9200); + /// + /// The for Windows 8. + /// + private static readonly Version Windows8Version = new Version(6, 2, 9200); - /// - /// Gets a value indicating whether the current operating system is Windows 8 or later. - /// - internal static bool IsWindows8OrLater + /// + /// Gets a value indicating whether the current operating system is Windows 8 or later. + /// + internal static bool IsWindows8OrLater + { + get { - get - { - return !ForceWindows7Mode - && Environment.OSVersion.Platform == PlatformID.Win32NT - && Environment.OSVersion.Version >= Windows8Version; - } + return !ForceWindows7Mode + && Environment.OSVersion.Platform == PlatformID.Win32NT + && Environment.OSVersion.Version >= Windows8Version; } } } diff --git a/src/Microsoft.VisualStudio.Threading/ListOfOftenOne`1.cs b/src/Microsoft.VisualStudio.Threading/ListOfOftenOne`1.cs index ec12f39b0..2549aeb50 100644 --- a/src/Microsoft.VisualStudio.Threading/ListOfOftenOne`1.cs +++ b/src/Microsoft.VisualStudio.Threading/ListOfOftenOne`1.cs @@ -1,260 +1,255 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Collections.Generic; +using System.Threading; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// A thread-safe collection optimized for very small number of non-null elements. +/// +/// The type of elements to be stored. +/// +/// The collection is alloc-free for storage, retrieval and enumeration of collection sizes of 0 or 1. +/// Beyond that causes one allocation for an immutable array that contains the entire collection. +/// +internal struct ListOfOftenOne : IEnumerable + where T : class { - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Linq; - using System.Text; - using System.Threading; - using System.Threading.Tasks; + /// + /// The single value or array of values stored by this collection. Null if empty. + /// + private object? value; /// - /// A thread-safe collection optimized for very small number of non-null elements. + /// Returns an enumerator for a current snapshot of the collection. /// - /// The type of elements to be stored. - /// - /// The collection is alloc-free for storage, retrieval and enumeration of collection sizes of 0 or 1. - /// Beyond that causes one allocation for an immutable array that contains the entire collection. - /// - internal struct ListOfOftenOne : IEnumerable - where T : class + public Enumerator GetEnumerator() { - /// - /// The single value or array of values stored by this collection. Null if empty. - /// - private object? value; - - /// - /// Returns an enumerator for a current snapshot of the collection. - /// - public Enumerator GetEnumerator() - { - return new Enumerator(Volatile.Read(ref this.value)); - } + return new Enumerator(Volatile.Read(ref this.value)); + } + + /// + /// Returns an enumerator for a current snapshot of the collection. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + /// + /// Returns an enumerator for a current snapshot of the collection. + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } - /// - /// Returns an enumerator for a current snapshot of the collection. - /// - IEnumerator IEnumerable.GetEnumerator() + /// + /// Adds an element to the collection. + /// + public void Add(T value) + { + object? priorValue; + object? fieldBeforeExchange; + do { - return this.GetEnumerator(); + priorValue = Volatile.Read(ref this.value); + object newValue = Combine(priorValue, value); + fieldBeforeExchange = Interlocked.CompareExchange(ref this.value, newValue, priorValue); } + while (priorValue != fieldBeforeExchange); + } - /// - /// Returns an enumerator for a current snapshot of the collection. - /// - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + /// + /// Removes an element from the collection. + /// + public void Remove(T value) + { + object? priorValue; + object? fieldBeforeExchange; + do { - return this.GetEnumerator(); + priorValue = Volatile.Read(ref this.value); + object? newValue = Remove(priorValue, value); + fieldBeforeExchange = Interlocked.CompareExchange(ref this.value, newValue, priorValue); } + while (priorValue != fieldBeforeExchange); + } - /// - /// Adds an element to the collection. - /// - public void Add(T value) + /// + /// Checks for reference equality between the specified value and an element of this collection. + /// + /// The value to check for. + /// if a match is found; otherwise. + /// + /// This method is intended to hide the Linq Contains extension method to avoid + /// the boxing of this struct and its Enumerator. + /// + public bool Contains(T value) + { + foreach (T? item in this) { - object? priorValue; - object? fieldBeforeExchange; - do + if (item == value) { - priorValue = Volatile.Read(ref this.value); - var newValue = Combine(priorValue, value); - fieldBeforeExchange = Interlocked.CompareExchange(ref this.value, newValue, priorValue); + return true; } - while (priorValue != fieldBeforeExchange); } - /// - /// Removes an element from the collection. - /// - public void Remove(T value) + return false; + } + + /// + /// Atomically clears the collection's contents and returns an enumerator over the prior contents. + /// + internal Enumerator EnumerateAndClear() + { + // Enumeration is atomically destructive. + object? enumeratedValue = Interlocked.Exchange(ref this.value, null); + return new Enumerator(enumeratedValue); + } + + /// + /// Combines the previous contents of the collection with one additional value. + /// + /// The collection's prior contents. + /// The value to add to the collection. + /// The new value to store as the collection. + private static object Combine(object? baseValue, T value) + { + Requires.NotNull(value, nameof(value)); + + if (baseValue is null) { - object? priorValue; - object? fieldBeforeExchange; - do - { - priorValue = Volatile.Read(ref this.value); - var newValue = Remove(priorValue, value); - fieldBeforeExchange = Interlocked.CompareExchange(ref this.value, newValue, priorValue); - } - while (priorValue != fieldBeforeExchange); + return value; } - /// - /// Checks for reference equality between the specified value and an element of this collection. - /// - /// The value to check for. - /// true if a match is found; false otherwise. - /// - /// This method is intended to hide the Linq Contains extension method to avoid - /// the boxing of this struct and its Enumerator. - /// - public bool Contains(T value) + if (baseValue is T singleValue) { - foreach (T? item in this) - { - if (item == value) - { - return true; - } - } + return new T[] { singleValue, value }; + } - return false; + var oldArray = (T[])baseValue; + var result = new T[oldArray.Length + 1]; + oldArray.CopyTo(result, 0); + result[result.Length - 1] = value; + return result; + } + + /// + /// Removes a value from contents of the collection. + /// + /// The collection's prior contents. + /// The value to remove from the collection. + /// The new value to store as the collection. + private static object? Remove(object? baseValue, T value) + { + if (baseValue == value || baseValue is null) + { + return null; } - /// - /// Atomically clears the collection's contents and returns an enumerator over the prior contents. - /// - internal Enumerator EnumerateAndClear() + if (baseValue is T) { - // Enumeration is atomically destructive. - object? enumeratedValue = Interlocked.Exchange(ref this.value, null); - return new Enumerator(enumeratedValue); + return baseValue; // the value to remove wasn't in the list anyway. } - /// - /// Combines the previous contents of the collection with one additional value. - /// - /// The collection's prior contents. - /// The value to add to the collection. - /// The new value to store as the collection. - private static object Combine(object? baseValue, T value) + var oldArray = (T[])baseValue; + int index = Array.IndexOf(oldArray, value); + if (index < 0) { - Requires.NotNull(value, nameof(value)); + return baseValue; + } + else if (oldArray.Length == 2) + { + return oldArray[index == 0 ? 1 : 0]; // return the one remaining value. + } + else + { + var result = new T[oldArray.Length - 1]; + Array.Copy(oldArray, result, index); + Array.Copy(oldArray, index + 1, result, index, result.Length - index); + return result; + } + } - if (baseValue is null) - { - return value; - } + public struct Enumerator : IEnumerator + { + private const int IndexBeforeFirstArrayElement = -1; + private const int IndexSingleElement = -2; + private const int IndexBeforeSingleElement = -3; - if (baseValue is T singleValue) - { - return new T[] { singleValue, value }; - } + private readonly object? enumeratedValue; - var oldArray = (T[])baseValue; - var result = new T[oldArray.Length + 1]; - oldArray.CopyTo(result, 0); - result[result.Length - 1] = value; - return result; - } + private int currentIndex; - /// - /// Removes a value from contents of the collection. - /// - /// The collection's prior contents. - /// The value to remove from the collection. - /// The new value to store as the collection. - private static object? Remove(object? baseValue, T value) + internal Enumerator(object? enumeratedValue) { - if (baseValue == value || baseValue is null) - { - return null; - } + this.enumeratedValue = enumeratedValue; + this.currentIndex = 0; + this.Reset(); + } - if (baseValue is T) + public T Current + { + get { - return baseValue; // the value to remove wasn't in the list anyway. - } + if (this.currentIndex == IndexBeforeFirstArrayElement || this.currentIndex == IndexBeforeSingleElement) + { + throw new InvalidOperationException(); + } - var oldArray = (T[])baseValue; - int index = Array.IndexOf(oldArray, value); - if (index < 0) - { - return baseValue; - } - else if (oldArray.Length == 2) - { - return oldArray[index == 0 ? 1 : 0]; // return the one remaining value. - } - else - { - var result = new T[oldArray.Length - 1]; - Array.Copy(oldArray, result, index); - Array.Copy(oldArray, index + 1, result, index, result.Length - index); - return result; + // enumeratedValue cannot be null here following a call to `MoveNext` that returns true (required + // for correct usage of IEnumerator). + return this.currentIndex == IndexSingleElement + ? (T)this.enumeratedValue! + : ((T[])this.enumeratedValue!)[this.currentIndex]; } } - public struct Enumerator : IEnumerator + object System.Collections.IEnumerator.Current { - private const int IndexBeforeFirstArrayElement = -1; - private const int IndexSingleElement = -2; - private const int IndexBeforeSingleElement = -3; - - private readonly object? enumeratedValue; + get { return this.Current; } + } - private int currentIndex; + public void Dispose() + { + } - internal Enumerator(object? enumeratedValue) + public bool MoveNext() + { + if (this.currentIndex == IndexBeforeSingleElement && this.enumeratedValue is object) { - this.enumeratedValue = enumeratedValue; - this.currentIndex = 0; - this.Reset(); + this.currentIndex = IndexSingleElement; + return true; } - public T Current + if (this.currentIndex == IndexSingleElement) { - get - { - if (this.currentIndex == IndexBeforeFirstArrayElement || this.currentIndex == IndexBeforeSingleElement) - { - throw new InvalidOperationException(); - } - - // enumeratedValue cannot be null here following a call to `MoveNext` that returns true (required - // for correct usage of IEnumerator). - return this.currentIndex == IndexSingleElement - ? (T)this.enumeratedValue! - : ((T[])this.enumeratedValue!)[this.currentIndex]; - } + return false; } - object System.Collections.IEnumerator.Current + if (this.currentIndex == IndexBeforeFirstArrayElement) { - get { return this.Current; } + this.currentIndex = 0; + return true; } - public void Dispose() + var array = (T[]?)this.enumeratedValue; + if (this.currentIndex >= 0 && this.currentIndex < array!.Length) { + this.currentIndex++; + return this.currentIndex < array.Length; } - public bool MoveNext() - { - if (this.currentIndex == IndexBeforeSingleElement && this.enumeratedValue is object) - { - this.currentIndex = IndexSingleElement; - return true; - } - - if (this.currentIndex == IndexSingleElement) - { - return false; - } - - if (this.currentIndex == IndexBeforeFirstArrayElement) - { - this.currentIndex = 0; - return true; - } - - var array = (T[]?)this.enumeratedValue; - if (this.currentIndex >= 0 && this.currentIndex < array!.Length) - { - this.currentIndex++; - return this.currentIndex < array.Length; - } - - return false; - } + return false; + } - public void Reset() - { - this.currentIndex = this.enumeratedValue is T[] ? IndexBeforeFirstArrayElement : IndexBeforeSingleElement; - } + public void Reset() + { + this.currentIndex = this.enumeratedValue is T[] ? IndexBeforeFirstArrayElement : IndexBeforeSingleElement; } } } diff --git a/src/Microsoft.VisualStudio.Threading/MemoryInspection.cs b/src/Microsoft.VisualStudio.Threading/MemoryInspection.cs new file mode 100644 index 000000000..b9f9ca85b --- /dev/null +++ b/src/Microsoft.VisualStudio.Threading/MemoryInspection.cs @@ -0,0 +1,252 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; + +[RequiresUnreferencedCode(Reasons.DiagnosticAnalysisOnly)] +internal static class MemoryInspection +{ + /// + /// The substring that should be inserted before each async return stack frame. + /// + /// + /// When printing synchronous callstacks, .NET begins each frame with " at ". + /// When printing async return stack, we use this to indicate continuations. + /// + private const string AsyncReturnStackPrefix = " -> "; + + /// + /// Walk the continuation objects inside "async state machines" to generate the return call stack. + /// FOR DIAGNOSTIC PURPOSES ONLY. + /// + /// The delegate that represents the head of an async continuation chain. + internal static IEnumerable GetAsyncReturnStackFrames(this Delegate continuationDelegate) + { + IAsyncStateMachine? stateMachine = FindAsyncStateMachine(continuationDelegate); + if (stateMachine is null) + { + // Did not find the async state machine, so returns the method name as top frame and stop walking. + yield return GetDelegateLabel(continuationDelegate); + yield break; + } + + do + { + object? state = GetStateMachineFieldValueOnSuffix(stateMachine, "__state"); + yield return string.Format( + CultureInfo.CurrentCulture, + "{2}{0} (state: {1}, address: 0x{3:X8})", + stateMachine.GetType().FullName, + state, + AsyncReturnStackPrefix, + (long)GetAddress(stateMachine)); // the long cast allows hex formatting + + Delegate[]? continuationDelegates = FindContinuationDelegates(stateMachine).ToArray(); + if (continuationDelegates.Length == 0) + { + break; + } + + // Consider: It's possible but uncommon scenario to have multiple "async methods" being awaiting for one "async method". + // Here we just choose the first awaiting "async method" as that should be good enough for postmortem. + // In future we might want to revisit this to cover the other awaiting "async methods". + stateMachine = continuationDelegates.Select((d) => FindAsyncStateMachine(d)) + .FirstOrDefault((s) => s is object); + if (stateMachine is null) + { + yield return GetDelegateLabel(continuationDelegates.First()); + } + } + while (stateMachine is object); + } + + /// + /// A helper method to get the label of the given delegate. + /// + private static string GetDelegateLabel(Delegate invokeDelegate) + { + Requires.NotNull(invokeDelegate, nameof(invokeDelegate)); + + MethodInfo? method = invokeDelegate.GetMethodInfo(); + if (invokeDelegate.Target is object) + { + string instanceType = string.Empty; + if (!(method?.DeclaringType?.Equals(invokeDelegate.Target.GetType()) ?? false)) + { + instanceType = " (" + invokeDelegate.Target.GetType().FullName + ")"; + } + + return string.Format( + CultureInfo.CurrentCulture, + "{3}{0}.{1}{2} (target address: 0x{4:X" + (IntPtr.Size * 2) + "})", + method?.DeclaringType?.FullName, + method?.Name, + instanceType, + AsyncReturnStackPrefix, + GetAddress(invokeDelegate.Target).ToInt64()); // the cast allows hex formatting + } + + return string.Format( + CultureInfo.CurrentCulture, + "{2}{0}.{1}", + method?.DeclaringType?.FullName, + method?.Name, + AsyncReturnStackPrefix); + } + + /// + /// A helper method to find the async state machine from the given delegate. + /// + private static IAsyncStateMachine? FindAsyncStateMachine(Delegate invokeDelegate) + { + Requires.NotNull(invokeDelegate, nameof(invokeDelegate)); + + if (invokeDelegate.Target is object) + { + // Some delegates are wrapped with a ContinuationWrapper object. We have to unwrap that in those cases. + // In testing, this m_continuation field jump is only required when the debugger is attached -- weird. + // I suspect however that it's a natural behavior of the async state machine (when there are >1 continuations perhaps). + // So we check for the case in all cases. + if (GetFieldValue(invokeDelegate.Target, "m_continuation") is Action continuation) + { + invokeDelegate = continuation; + if (invokeDelegate.Target is null) + { + return null; + } + } + + var stateMachine = GetFieldValue(invokeDelegate.Target, "m_stateMachine") as IAsyncStateMachine; + return stateMachine; + } + + return null; + } + + /// + /// This is the core to find the continuation delegate(s) inside the given async state machine. + /// The chain of objects is like this: async state machine -> async method builder -> task -> continuation object -> action. + /// + /// + /// There are 3 types of "async method builder": AsyncVoidMethodBuilder, AsyncTaskMethodBuilder, AsyncTaskMethodBuilder<T>. + /// We don't cover AsyncVoidMethodBuilder as it is used rarely and it can't be awaited either; + /// AsyncTaskMethodBuilder is a wrapper on top of AsyncTaskMethodBuilder<VoidTaskResult>. + /// + private static IEnumerable FindContinuationDelegates(IAsyncStateMachine stateMachine) + { + Requires.NotNull(stateMachine, nameof(stateMachine)); + + object? builder = GetStateMachineFieldValueOnSuffix(stateMachine, "__builder"); + if (builder is null) + { + yield break; + } + + object? task = GetFieldValue(builder, "m_task"); + if (task is null) + { + // Probably this builder is an instance of "AsyncTaskMethodBuilder", so we need to get its inner "AsyncTaskMethodBuilder" + builder = GetFieldValue(builder, "m_builder"); + if (builder is object) + { + task = GetFieldValue(builder, "m_task"); + } + } + + if (task is null) + { + yield break; + } + + // "task" might be an instance of the type deriving from "Task", but "m_continuationObject" is a private field in "Task", + // so we need to use "typeof(Task)" to access "m_continuationObject". + FieldInfo? continuationField = typeof(Task).GetTypeInfo().GetDeclaredField("m_continuationObject"); + if (continuationField is null) + { + yield break; + } + + object? continuationObject = continuationField.GetValue(task); + if (continuationObject is null) + { + yield break; + } + + if (continuationObject is IEnumerable items) + { + foreach (object? item in items) + { + Delegate? action = item as Delegate ?? GetFieldValue(item!, "m_action") as Delegate; + if (action is object) + { + yield return action; + } + } + } + else + { + Delegate? action = continuationObject as Delegate ?? GetFieldValue(continuationObject, "m_action") as Delegate; + if (action is object) + { + yield return action; + } + } + } + + /// + /// A helper method to get field's value given the object and the field name. + /// + private static object? GetFieldValue(object obj, string fieldName) + { + Requires.NotNull(obj, nameof(obj)); + Requires.NotNullOrEmpty(fieldName, nameof(fieldName)); + + FieldInfo? field = obj.GetType().GetTypeInfo().GetDeclaredField(fieldName); + if (field is object) + { + return field.GetValue(obj); + } + + return null; + } + + /// + /// The field names of "async state machine" are not fixed; the workaround is to find the field based on the suffix. + /// + private static object? GetStateMachineFieldValueOnSuffix(IAsyncStateMachine stateMachine, string suffix) + { + Requires.NotNull(stateMachine, nameof(stateMachine)); + Requires.NotNullOrEmpty(suffix, nameof(suffix)); + + IEnumerable? fields = stateMachine.GetType().GetTypeInfo().DeclaredFields; + FieldInfo? field = fields.FirstOrDefault((f) => f.Name.EndsWith(suffix, StringComparison.Ordinal)); + if (field is object) + { + return field.GetValue(stateMachine); + } + + return null; + } + + /// + /// Gets the memory address of a given object. + /// + /// The object to get the address for. + /// The memory address. + /// + /// This method works when GCHandle will refuse because the type of object is a non-blittable type. + /// However, this method provides no guarantees that the address will remain valid for the caller, + /// so it is only useful for diagnostics and when we don't expect addresses to be changing much any more. + /// + private static unsafe IntPtr GetAddress(object value) => new IntPtr(Unsafe.AsPointer(ref value)); +} diff --git a/src/Microsoft.VisualStudio.Threading/Microsoft.VisualStudio.Threading.csproj b/src/Microsoft.VisualStudio.Threading/Microsoft.VisualStudio.Threading.csproj index ae5b3dcb6..5e801bf84 100644 --- a/src/Microsoft.VisualStudio.Threading/Microsoft.VisualStudio.Threading.csproj +++ b/src/Microsoft.VisualStudio.Threading/Microsoft.VisualStudio.Threading.csproj @@ -1,30 +1,21 @@  + - netstandard2.0;netcoreapp3.1;net472 - - Async synchronization primitives, async collections, TPL and dataflow extensions. - Async synchronization primitives, async collections, TPL and dataflow extensions. The JoinableTaskFactory allows synchronously blocking the UI thread for async work. This package is applicable to any .NET application (not just Visual Studio). - Threading Async Lock Synchronization Threadsafe - + Microsoft.VisualStudio.Threading.Only + $(Description) + This package contains only the library, without a dependency on the analyzers. + Use the Microsoft.VisualStudio.Threading package to get the library and analyzers together. + true + + true + true + System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute + 17.14.15 + + + true - - - ResXFileCodeGenerator - Strings.Designer.cs - - - Strings.resx - - - - - - True - True - Strings.resx - - @@ -32,21 +23,13 @@ - - - - - - - - - - - - - - + + + + + + + - + diff --git a/src/Microsoft.VisualStudio.Threading/NativeMethods.cs b/src/Microsoft.VisualStudio.Threading/NativeMethods.cs deleted file mode 100644 index faeb4895d..000000000 --- a/src/Microsoft.VisualStudio.Threading/NativeMethods.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Microsoft.VisualStudio.Threading -{ - using System; - using System.Runtime.InteropServices; - using Microsoft.Win32.SafeHandles; - - /// - /// P/Invoke methods. - /// - internal static partial class NativeMethods - { - /// - /// Indicates that the lifetime of the registration must not be tied to the lifetime of the thread issuing the RegNotifyChangeKeyValue call. - /// Note: This flag value is only supported in Windows 8 and later. - /// - internal const RegistryChangeNotificationFilters REG_NOTIFY_THREAD_AGNOSTIC = (RegistryChangeNotificationFilters)0x10000000L; - - /// - /// Really truly non pumping wait. - /// Raw IntPtrs have to be used, because the marshaller does not support arrays of SafeHandle, only - /// single SafeHandles. - /// - /// The number of handles in the array. - /// The handles to wait for. - /// A flag indicating whether all handles must be signaled before returning. - /// A timeout that will cause this method to return. - [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] - internal static extern int WaitForMultipleObjects(uint handleCount, IntPtr[] waitHandles, [MarshalAs(UnmanagedType.Bool)] bool waitAll, uint millisecondsTimeout); - - /// - /// Registers to receive notification of changes to a registry key. - /// - /// The handle to the registry key to watch. - /// true to watch the keys descendent keys as well; false to watch only this key without descendents. - /// The types of changes to watch for. - /// A handle to the event to set when a change occurs. - /// If this parameter is TRUE, the function returns immediately and reports changes by signaling the specified event. If this parameter is FALSE, the function does not return until a change has occurred. - /// A win32 error code. ERROR_SUCCESS (0) if successful. - [DllImport("Advapi32.dll", ExactSpelling = true, SetLastError = true)] - internal static extern int RegNotifyChangeKeyValue( - SafeRegistryHandle hKey, - [MarshalAs(UnmanagedType.Bool)] bool watchSubtree, - RegistryChangeNotificationFilters notifyFilter, - SafeWaitHandle hEvent, - [MarshalAs(UnmanagedType.Bool)] bool asynchronous); - } -} diff --git a/src/Microsoft.VisualStudio.Threading/NativeMethods.txt b/src/Microsoft.VisualStudio.Threading/NativeMethods.txt new file mode 100644 index 000000000..1f4cb72fd --- /dev/null +++ b/src/Microsoft.VisualStudio.Threading/NativeMethods.txt @@ -0,0 +1,2 @@ +WaitForMultipleObjects +RegNotifyChangeKeyValue diff --git a/src/Microsoft.VisualStudio.Threading/NoMessagePumpSyncContext.cs b/src/Microsoft.VisualStudio.Threading/NoMessagePumpSyncContext.cs index 8f89a2ebc..8bd72aab3 100644 --- a/src/Microsoft.VisualStudio.Threading/NoMessagePumpSyncContext.cs +++ b/src/Microsoft.VisualStudio.Threading/NoMessagePumpSyncContext.cs @@ -1,65 +1,121 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Runtime.InteropServices; +using System.Threading; +using global::Windows.Win32; +using global::Windows.Win32.Foundation; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// A SynchronizationContext whose synchronously blocking Wait method does not allow +/// any reentrancy via the message pump. +/// +public class NoMessagePumpSyncContext : SynchronizationContext { - using System; - using System.Runtime.InteropServices; - using System.Threading; + /// + /// A shared singleton. + /// + private static readonly SynchronizationContext DefaultInstance = new NoMessagePumpSyncContext(); + + private readonly SynchronizationContext? underlyingSyncContext; + + /// + /// Initializes a new instance of the class. + /// + /// + /// When using this constructor, uses the default + /// behavior and schedules work on the thread pool, while uses the default + /// behavior and invokes the callback synchronously on the calling thread. + /// + public NoMessagePumpSyncContext() + { + // This is required so that our override of Wait is invoked. + this.SetWaitNotificationRequired(); + } /// - /// A SynchronizationContext whose synchronously blocking Wait method does not allow - /// any reentrancy via the message pump. + /// Initializes a new instance of the class. /// - public class NoMessagePumpSyncContext : SynchronizationContext + /// The that should handle calls to and . + public NoMessagePumpSyncContext(SynchronizationContext underlyingSyncContext) + : this() + { + Requires.NotNull(underlyingSyncContext, nameof(underlyingSyncContext)); + this.underlyingSyncContext = underlyingSyncContext; + } + + /// + /// Gets a shared instance of this class. + /// + public static SynchronizationContext Default + { + get { return DefaultInstance; } + } + + /// + public override void Send(SendOrPostCallback d, object? state) { - /// - /// A shared singleton. - /// - private static readonly SynchronizationContext DefaultInstance = new NoMessagePumpSyncContext(); + Requires.NotNull(d, nameof(d)); - /// - /// Initializes a new instance of the class. - /// - public NoMessagePumpSyncContext() + if (this.underlyingSyncContext is { } underlying) { - // This is required so that our override of Wait is invoked. - this.SetWaitNotificationRequired(); + underlying.Send(d, state); } - - /// - /// Gets a shared instance of this class. - /// - public static SynchronizationContext Default + else { - get { return DefaultInstance; } + base.Send(d, state); } + } - /// - /// Synchronously blocks without a message pump. - /// - /// An array of type that contains the native operating system handles. - /// true to wait for all handles; false to wait for any handle. - /// The number of milliseconds to wait, or (-1) to wait indefinitely. - /// - /// The array index of the object that satisfied the wait. - /// - public override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout) + /// + public override void Post(SendOrPostCallback d, object? state) + { + Requires.NotNull(d, nameof(d)); + + if (this.underlyingSyncContext is { } underlying) { - Requires.NotNull(waitHandles, nameof(waitHandles)); + underlying.Post(d, state); + } + else + { + base.Post(d, state); + } + } - // On .NET Framework we must take special care to NOT end up in a call to CoWait (which lets in RPC calls). - // Off Windows, we can't p/invoke to kernel32, but it appears that .NET Core never calls CoWait, so we can rely on default behavior. - // We're just going to use the OS as the switch instead of the framework so that (one day) if we drop our .NET Framework specific target, - // and if .NET Core ever adds CoWait support on Windows, we'll still behave properly. - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return NativeMethods.WaitForMultipleObjects((uint)waitHandles.Length, waitHandles, waitAll, (uint)millisecondsTimeout); - } - else + /// + /// Synchronously blocks without a message pump. + /// + /// An array of type that contains the native operating system handles. + /// true to wait for all handles; false to wait for any handle. + /// The number of milliseconds to wait, or (-1) to wait indefinitely. + /// + /// The array index of the object that satisfied the wait. + /// + public override unsafe int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout) + { + Requires.NotNull(waitHandles, nameof(waitHandles)); + + // On .NET Framework we must take special care to NOT end up in a call to CoWait (which lets in RPC calls). + // Off Windows, we can't p/invoke to kernel32, but it appears that .NET never calls CoWait, so we can rely on default behavior. + // We're just going to use the OS as the switch instead of the runtime so that (one day) if we drop our .NET Framework specific target, + // and if .NET ever adds CoWait support on Windows, we'll still behave properly. +#if NET + if (OperatingSystem.IsWindowsVersionAtLeast(5, 1, 2600)) +#else + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) +#endif + { + fixed (IntPtr* pHandles = waitHandles) { - return WaitHelper(waitHandles, waitAll, millisecondsTimeout); + return (int)PInvoke.WaitForMultipleObjects((uint)waitHandles.Length, (HANDLE*)pHandles, waitAll, (uint)millisecondsTimeout); } } + else + { + return WaitHelper(waitHandles, waitAll, millisecondsTimeout); + } } } diff --git a/src/Microsoft.VisualStudio.Threading/NonConcurrentSynchronizationContext.cs b/src/Microsoft.VisualStudio.Threading/NonConcurrentSynchronizationContext.cs index 9a8483e00..8e006891c 100644 --- a/src/Microsoft.VisualStudio.Threading/NonConcurrentSynchronizationContext.cs +++ b/src/Microsoft.VisualStudio.Threading/NonConcurrentSynchronizationContext.cs @@ -1,177 +1,176 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// A that executes messages in the order they are received. +/// +/// +/// Delegates will be invoked in the order they are received on the threadpool. +/// No two delegates will ever be executed concurrently, but may permit +/// a delegate to execute inline on another. +/// Note that if the delegate invokes an async method, the delegate formally ends +/// when the async method yields for the first time or returns, whichever comes first. +/// Once that delegate returns the next delegate can be executed. +/// +public sealed class NonConcurrentSynchronizationContext : SynchronizationContext { - using System; - using System.Diagnostics.CodeAnalysis; - using System.Threading; - using System.Threading.Tasks; + /// + /// The queue of work to execute, if this is the original instance. + /// + private readonly AsyncQueue<(SendOrPostCallback, object?)>? queue; + + /// + /// A value indicating whether to set this instance as when invoking delegates. + /// + private readonly bool sticky; + + /// + /// The original instance (on which was called). + /// + private readonly NonConcurrentSynchronizationContext? copyOf; /// - /// A that executes messages in the order they are received. + /// Set to the when a delegate is currently executing. + /// + private int? activeManagedThreadId; + + /// + /// Initializes a new instance of the class. + /// + /// + /// A value indicating whether to set this instance as when invoking delegates. + /// This has the effect that async methods that are invoked on this + /// will execute their continuations on this as well unless they use with as the argument. + /// + public NonConcurrentSynchronizationContext(bool sticky) + { + this.queue = new AsyncQueue<(SendOrPostCallback, object?)>(); + this.sticky = sticky; + + // Start the queue processor. It will handle all exceptions. + this.ProcessQueueAsync().Forget(); + } + + /// + /// Initializes a new instance of the class + /// that is a copy of an existing instance. + /// + /// The instance to copy from. + private NonConcurrentSynchronizationContext(NonConcurrentSynchronizationContext copyFrom) + { + // We do *not* kick off processing of the queue, since the original copy has done that and we don't want two logical threads processing the queue. + this.sticky = copyFrom.sticky; + this.copyOf = copyFrom; + } + + /// + /// Occurs when posted work throws an unhandled exception. /// /// - /// Delegates will be invoked in the order they are received on the threadpool. - /// No two delegates will ever be executed concurrently, but may permit - /// a delegate to execute inline on another. - /// Note that if the delegate invokes an async method, the delegate formally ends - /// when the async method yields for the first time or returns, whichever comes first. - /// Once that delegate returns the next delegate can be executed. + /// Any exception thrown from this handler will crash the process. /// - public sealed class NonConcurrentSynchronizationContext : SynchronizationContext - { - /// - /// The queue of work to execute, if this is the original instance. - /// - private readonly AsyncQueue<(SendOrPostCallback, object?)>? queue; - - /// - /// A value indicating whether to set this instance as when invoking delegates. - /// - private readonly bool sticky; - - /// - /// The original instance (on which was called). - /// - private readonly NonConcurrentSynchronizationContext? copyOf; - - /// - /// Set to the when a delegate is currently executing. - /// - private int? activeManagedThreadId; - - /// - /// Initializes a new instance of the class. - /// - /// - /// A value indicating whether to set this instance as when invoking delegates. - /// This has the effect that async methods that are invoked on this - /// will execute their continuations on this as well unless they use with false as the argument. - /// - public NonConcurrentSynchronizationContext(bool sticky) - { - this.queue = new AsyncQueue<(SendOrPostCallback, object?)>(); - this.sticky = sticky; + public event EventHandler? UnhandledException; - // Start the queue processor. It will handle all exceptions. - this.ProcessQueueAsync().Forget(); - } + /// + public override void Post(SendOrPostCallback d, object? state) + { + Requires.NotNull(d, nameof(d)); - /// - /// Initializes a new instance of the class - /// that is a copy of an existing instance. - /// - /// The instance to copy from. - private NonConcurrentSynchronizationContext(NonConcurrentSynchronizationContext copyFrom) + if (this.copyOf is object) { - // We do *not* kick off processing of the queue, since the original copy has done that and we don't want two logical threads processing the queue. - this.sticky = copyFrom.sticky; - this.copyOf = copyFrom; + this.copyOf.Post(d, state); + return; } - /// - /// Occurs when posted work throws an unhandled exception. - /// - /// - /// Any exception thrown from this handler will crash the process. - /// - public event EventHandler? UnhandledException; - - /// - public override void Post(SendOrPostCallback d, object? state) - { - Requires.NotNull(d, nameof(d)); + this.queue!.Enqueue((d, state)); + } - if (this.copyOf is object) - { - this.copyOf.Post(d, state); - return; - } + /// + public override void Send(SendOrPostCallback d, object? state) + { + Requires.NotNull(d, nameof(d)); - this.queue!.Enqueue((d, state)); + if (this.copyOf is object) + { + this.copyOf.Send(d, state); + return; } - /// - public override void Send(SendOrPostCallback d, object? state) + // Allow inlining if we're on the already-assigned thread. + if (this.activeManagedThreadId is int activeThread && Environment.CurrentManagedThreadId == activeThread) { - Requires.NotNull(d, nameof(d)); - - if (this.copyOf is object) + using (this.sticky ? this.Apply(checkForChangesOnRevert: false) : default) { - this.copyOf.Send(d, state); - return; + d(state); } - // Allow inlining if we're on the already-assigned thread. - if (this.activeManagedThreadId is int activeThread && Environment.CurrentManagedThreadId == activeThread) + return; + } + + var tcs = new TaskCompletionSource(); + this.Post( + s2 => { - using (this.sticky ? this.Apply(checkForChangesOnRevert: false) : default) + (SendOrPostCallback cb, object s, TaskCompletionSource m) = (Tuple>)s2!; + try { - d(state); + cb(s); + m.SetResult(null); } - - return; - } - - var tcs = new TaskCompletionSource(); - this.Post( - s2 => + catch (Exception ex) { - (SendOrPostCallback cb, object s, TaskCompletionSource m) = (Tuple>)s2!; - try - { - cb(s); - m.SetResult(null); - } - catch (Exception ex) - { - m.SetException(ex); - } - }, - Tuple.Create(d, state, tcs)); - tcs.Task.GetAwaiter().GetResult(); - } + m.SetException(ex); + } + }, + Tuple.Create(d, state, tcs)); + tcs.Task.GetAwaiter().GetResult(); + } - /// - public override SynchronizationContext CreateCopy() => new NonConcurrentSynchronizationContext(this); + /// + public override SynchronizationContext CreateCopy() => new NonConcurrentSynchronizationContext(this); - /// - /// Executes queued work on the threadpool, one at a time. - /// - /// A task that always completes successfully. - private async Task ProcessQueueAsync() + /// + /// Executes queued work on the threadpool, one at a time. + /// + /// A task that always completes successfully. + private async Task ProcessQueueAsync() + { + try { - try + while (true) { - while (true) + (SendOrPostCallback, object?) work = await this.queue!.DequeueAsync().ConfigureAwait(false); + this.activeManagedThreadId = Environment.CurrentManagedThreadId; + try { - (SendOrPostCallback, object?) work = await this.queue!.DequeueAsync().ConfigureAwait(false); - this.activeManagedThreadId = Environment.CurrentManagedThreadId; - try - { - using (this.sticky ? this.Apply(checkForChangesOnRevert: false) : default) - { - work.Item1(work.Item2); - } - } - catch (Exception ex) - { - this.UnhandledException?.Invoke(this, ex); - } - finally + using (this.sticky ? this.Apply(checkForChangesOnRevert: false) : default) { - this.activeManagedThreadId = null; + work.Item1(work.Item2); } } + catch (Exception ex) + { + this.UnhandledException?.Invoke(this, ex); + } + finally + { + this.activeManagedThreadId = null; + } } - catch (Exception ex) - { - // A failure to schedule work is fatal because it can lead to hangs that are - // very hard to diagnose to a failure in the scheduler, and even harder to identify - // the root cause of the failure in the scheduler. - Environment.FailFast("Failure in scheduler.", ex); - } + } + catch (Exception ex) + { + // A failure to schedule work is fatal because it can lead to hangs that are + // very hard to diagnose to a failure in the scheduler, and even harder to identify + // the root cause of the failure in the scheduler. + Environment.FailFast("Failure in scheduler.", ex); } } } diff --git a/src/Microsoft.VisualStudio.Threading/NullableHelpers.cs b/src/Microsoft.VisualStudio.Threading/NullableHelpers.cs index c5600777f..2be3d6dd8 100644 --- a/src/Microsoft.VisualStudio.Threading/NullableHelpers.cs +++ b/src/Microsoft.VisualStudio.Threading/NullableHelpers.cs @@ -1,37 +1,36 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading -{ - using System; +using System; + +namespace Microsoft.VisualStudio.Threading; - internal static class NullableHelpers +internal static class NullableHelpers +{ + /// + /// Converts a delegate which assumes an argument that is never null into a delegate which might be given a null value, + /// without adding an explicit null check. + /// + /// The type of argument to be passed to the delegate. + /// The delegate which, according to the signature, does not expect . + /// The exact same referenced delegate, but with a signature that may expect . + internal static Action AsNullableArgAction(Action action) + where T : class { - /// - /// Converts a delegate which assumes an argument that is never null into a delegate which might be given a null value, - /// without adding an explicit null check. - /// - /// The type of argument to be passed to the delegate. - /// The delegate which, according to the signature, does not expect . - /// The exact same referenced delegate, but with a signature that may expect . - internal static Action AsNullableArgAction(Action action) - where T : class - { - return action!; - } + return action!; + } - /// - /// Converts a delegate which assumes an argument that is never null into a delegate which might be given a null value, - /// without adding an explicit null check. - /// - /// The type of argument to be passed to the delegate. - /// The type of value returned from the delegate. - /// The delegate which, according to the signature, does not expect . - /// The exact same referenced delegate, but with a signature that may expect . - internal static Func AsNullableArgFunc(Func func) - where TArg : class - { - return func!; - } + /// + /// Converts a delegate which assumes an argument that is never null into a delegate which might be given a null value, + /// without adding an explicit null check. + /// + /// The type of argument to be passed to the delegate. + /// The type of value returned from the delegate. + /// The delegate which, according to the signature, does not expect . + /// The exact same referenced delegate, but with a signature that may expect . + internal static Func AsNullableArgFunc(Func func) + where TArg : class + { + return func!; } } diff --git a/src/Microsoft.VisualStudio.Threading/ProgressWithCompletion`1.cs b/src/Microsoft.VisualStudio.Threading/ProgressWithCompletion`1.cs index 6facff82f..959fa1324 100644 --- a/src/Microsoft.VisualStudio.Threading/ProgressWithCompletion`1.cs +++ b/src/Microsoft.VisualStudio.Threading/ProgressWithCompletion`1.cs @@ -1,235 +1,234 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// An incremental progress reporting mechanism that also allows +/// asynchronous awaiting for all reports to be processed. +/// +/// The type of message sent in progress updates. +public class ProgressWithCompletion : IProgress { - using System; - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - using System.Threading; - using System.Threading.Tasks; + /// + /// The synchronization object. + /// Applicable only when is null. + /// + private readonly object? syncObject; /// - /// An incremental progress reporting mechanism that also allows - /// asynchronous awaiting for all reports to be processed. + /// The handler to invoke for each progress update. /// - /// The type of message sent in progress updates. - public class ProgressWithCompletion : IProgress + private readonly Func handler; + + /// + /// The set of progress reports that have started (but may not have finished yet). + /// Applicable only when is null. + /// + private readonly HashSet? outstandingTasks; + + /// + /// The factory to use for invoking the . + /// Applicable only when is null. + /// + private readonly TaskFactory? taskFactory; + + /// + /// A value indicating whether this instance was constructed on the main thread. + /// Applicable only when is not null. + /// + private readonly bool createdOnMainThread; + + /// + /// The to use when invoking the to mitigate deadlocks. + /// May be null. + /// + private readonly JoinableTaskFactory? joinableTaskFactory; + + /// + /// A collection of outstanding progress updates that have not completed execution. + /// Applicable only when is not null. + /// + private readonly JoinableTaskCollection? outstandingJoinableTasks; + + /// + /// Initializes a new instance of the class. + /// + /// + /// A handler to invoke for each reported progress value. + /// Depending on the instance that is captured when this constructor is invoked, + /// it is possible that this handler instance could be invoked concurrently with itself. + /// + public ProgressWithCompletion(Action handler) + : this(WrapSyncHandler(handler), joinableTaskFactory: null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// A handler to invoke for each reported progress value. + /// Depending on the instance that is captured when this constructor is invoked, + /// it is possible that this handler instance could be invoked concurrently with itself. + /// + public ProgressWithCompletion(Func handler) + : this(handler, joinableTaskFactory: null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// A handler to invoke for each reported progress value. + /// It is possible that this handler instance could be invoked concurrently with itself. + /// + /// A instance that can be used to mitigate deadlocks when is called and the requires the main thread. + public ProgressWithCompletion(Action handler, JoinableTaskFactory? joinableTaskFactory) + : this(WrapSyncHandler(handler), joinableTaskFactory) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// A handler to invoke for each reported progress value. + /// It is possible that this handler instance could be invoked concurrently with itself. + /// + /// A instance that can be used to mitigate deadlocks when is called and the requires the main thread. + public ProgressWithCompletion(Func handler, JoinableTaskFactory? joinableTaskFactory) { - /// - /// The synchronization object. - /// Applicable only when is null. - /// - private readonly object? syncObject; - - /// - /// The handler to invoke for each progress update. - /// - private readonly Func handler; - - /// - /// The set of progress reports that have started (but may not have finished yet). - /// Applicable only when is null. - /// - private readonly HashSet? outstandingTasks; - - /// - /// The factory to use for invoking the . - /// Applicable only when is null. - /// - private readonly TaskFactory? taskFactory; - - /// - /// A value indicating whether this instance was constructed on the main thread. - /// Applicable only when is not null. - /// - private readonly bool createdOnMainThread; - - /// - /// The to use when invoking the to mitigate deadlocks. - /// May be null. - /// - private readonly JoinableTaskFactory? joinableTaskFactory; - - /// - /// A collection of outstanding progress updates that have not completed execution. - /// Applicable only when is not null. - /// - private readonly JoinableTaskCollection? outstandingJoinableTasks; - - /// - /// Initializes a new instance of the class. - /// - /// - /// A handler to invoke for each reported progress value. - /// Depending on the instance that is captured when this constructor is invoked, - /// it is possible that this handler instance could be invoked concurrently with itself. - /// - public ProgressWithCompletion(Action handler) - : this(WrapSyncHandler(handler), joinableTaskFactory: null) + Requires.NotNull(handler, nameof(handler)); + this.handler = handler; + if (joinableTaskFactory is object) { + this.joinableTaskFactory = joinableTaskFactory; + this.outstandingJoinableTasks = joinableTaskFactory.Context.CreateCollection(); + this.createdOnMainThread = joinableTaskFactory.Context.IsOnMainThread; } - - /// - /// Initializes a new instance of the class. - /// - /// - /// A handler to invoke for each reported progress value. - /// Depending on the instance that is captured when this constructor is invoked, - /// it is possible that this handler instance could be invoked concurrently with itself. - /// - public ProgressWithCompletion(Func handler) - : this(handler, joinableTaskFactory: null) + else { + this.syncObject = new object(); + this.taskFactory = new TaskFactory(SynchronizationContext.Current is object ? TaskScheduler.FromCurrentSynchronizationContext() : TaskScheduler.Default); + this.outstandingTasks = new HashSet(); } + } - /// - /// Initializes a new instance of the class. - /// - /// - /// A handler to invoke for each reported progress value. - /// It is possible that this handler instance could be invoked concurrently with itself. - /// - /// A instance that can be used to mitigate deadlocks when is called and the requires the main thread. - public ProgressWithCompletion(Action handler, JoinableTaskFactory? joinableTaskFactory) - : this(WrapSyncHandler(handler), joinableTaskFactory) + /// + /// Receives a progress update. + /// + /// The value representing the updated progress. + void IProgress.Report(T value) + { + this.Report(value); + } + + /// + /// Returns a task that completes when all reported progress has executed. + /// + /// A task that completes when all progress is complete. + public Task WaitAsync() => this.WaitAsync(CancellationToken.None); + + /// + /// Returns a task that completes when all reported progress has executed. + /// + /// A cancellation token. + /// A task that completes when all progress is complete. + public Task WaitAsync(CancellationToken cancellationToken) + { + if (this.IsJoinableTaskAware(out _, out JoinableTaskCollection? outstandingJoinableTasks, out var syncObject, out HashSet? outstandingTasks, out _)) { + return outstandingJoinableTasks.JoinTillEmptyAsync(cancellationToken); } - - /// - /// Initializes a new instance of the class. - /// - /// - /// A handler to invoke for each reported progress value. - /// It is possible that this handler instance could be invoked concurrently with itself. - /// - /// A instance that can be used to mitigate deadlocks when is called and the requires the main thread. - public ProgressWithCompletion(Func handler, JoinableTaskFactory? joinableTaskFactory) + else { - Requires.NotNull(handler, nameof(handler)); - this.handler = handler; - if (joinableTaskFactory is object) + lock (syncObject) { - this.joinableTaskFactory = joinableTaskFactory; - this.outstandingJoinableTasks = joinableTaskFactory.Context.CreateCollection(); - this.createdOnMainThread = joinableTaskFactory.Context.IsOnMainThread; + return Task.WhenAll(outstandingTasks).WithCancellation(cancellationToken); } - else - { - this.syncObject = new object(); - this.taskFactory = new TaskFactory(SynchronizationContext.Current is object ? TaskScheduler.FromCurrentSynchronizationContext() : TaskScheduler.Default); - this.outstandingTasks = new HashSet(); - } - } - - /// - /// Receives a progress update. - /// - /// The value representing the updated progress. - void IProgress.Report(T value) - { - this.Report(value); } + } - /// - /// Returns a task that completes when all reported progress has executed. - /// - /// A task that completes when all progress is complete. - public Task WaitAsync() => this.WaitAsync(CancellationToken.None); - - /// - /// Returns a task that completes when all reported progress has executed. - /// - /// A cancellation token. - /// A task that completes when all progress is complete. - public Task WaitAsync(CancellationToken cancellationToken) + /// + /// Receives a progress update. + /// + /// The value representing the updated progress. + protected virtual void Report(T value) + { + if (this.IsJoinableTaskAware(out JoinableTaskFactory? joinableTaskFactory, out JoinableTaskCollection? outstandingJoinableTasks, out var syncObject, out HashSet? outstandingTasks, out TaskFactory? taskFactory)) { - if (this.IsJoinableTaskAware(out _, out JoinableTaskCollection? outstandingJoinableTasks, out var syncObject, out HashSet? outstandingTasks, out _)) - { - return outstandingJoinableTasks.JoinTillEmptyAsync(cancellationToken); - } - else - { - lock (syncObject) + JoinableTask joinableTask = joinableTaskFactory.RunAsync( + async delegate { - return Task.WhenAll(outstandingTasks).WithCancellation(cancellationToken); - } - } - } + // Emulate the behavior of having captured a SynchronizationContext by invoking the handler on the main thread + // if the constructor was on the main thread. Otherwise use the threadpool thread. But never invoke the handler + // inline with our caller, per the behavior folks expect from this and .NET's Progress class. + if (this.createdOnMainThread) + { + await joinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true); + } + else + { + await TaskScheduler.Default.SwitchTo(alwaysYield: true); + } - /// - /// Receives a progress update. - /// - /// The value representing the updated progress. - protected virtual void Report(T value) + await this.handler(value).ConfigureAwaitRunInline(); + }); + outstandingJoinableTasks.Add(joinableTask); + } + else { - if (this.IsJoinableTaskAware(out JoinableTaskFactory? joinableTaskFactory, out JoinableTaskCollection? outstandingJoinableTasks, out var syncObject, out HashSet? outstandingTasks, out TaskFactory? taskFactory)) + Task? reported = taskFactory.StartNew(() => this.handler(value)).Unwrap(); + lock (syncObject) { - JoinableTask joinableTask = joinableTaskFactory.RunAsync( - async delegate - { - // Emulate the behavior of having captured a SynchronizationContext by invoking the handler on the main thread - // if the constructor was on the main thread. Otherwise use the threadpool thread. But never invoke the handler - // inline with our caller, per the behavior folks expect from this and .NET's Progress class. - if (this.createdOnMainThread) - { - await joinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true); - } - else - { - await TaskScheduler.Default.SwitchTo(alwaysYield: true); - } - - await this.handler(value).ConfigureAwaitRunInline(); - }); - outstandingJoinableTasks.Add(joinableTask); + outstandingTasks.Add(reported); } - else - { - Task? reported = taskFactory.StartNew(() => this.handler(value)).Unwrap(); - lock (syncObject) - { - outstandingTasks.Add(reported); - } - reported.ContinueWith( - t => + reported.ContinueWith( + t => + { + lock (syncObject) { - lock (syncObject) - { - outstandingTasks.Remove(t); - } - }, - CancellationToken.None, - TaskContinuationOptions.NotOnFaulted | TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default); - } + outstandingTasks.Remove(t); + } + }, + CancellationToken.None, + TaskContinuationOptions.NotOnFaulted | TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); } + } - private static Func WrapSyncHandler(Action handler) + private static Func WrapSyncHandler(Action handler) + { + Requires.NotNull(handler, nameof(handler)); + return value => { - Requires.NotNull(handler, nameof(handler)); - return value => - { - handler(value); - return Task.CompletedTask; - }; - } + handler(value); + return Task.CompletedTask; + }; + } - private bool IsJoinableTaskAware( - [NotNullWhen(true)] out JoinableTaskFactory? joinableTaskFactory, - [NotNullWhen(true)] out JoinableTaskCollection? outstandingJoinableTasks, - [NotNullWhen(false)] out object? syncObject, - [NotNullWhen(false)] out HashSet? outstandingTasks, - [NotNullWhen(false)] out TaskFactory? taskFactory) - { - joinableTaskFactory = this.joinableTaskFactory; - outstandingJoinableTasks = this.outstandingJoinableTasks; + private bool IsJoinableTaskAware( + [NotNullWhen(true)] out JoinableTaskFactory? joinableTaskFactory, + [NotNullWhen(true)] out JoinableTaskCollection? outstandingJoinableTasks, + [NotNullWhen(false)] out object? syncObject, + [NotNullWhen(false)] out HashSet? outstandingTasks, + [NotNullWhen(false)] out TaskFactory? taskFactory) + { + joinableTaskFactory = this.joinableTaskFactory; + outstandingJoinableTasks = this.outstandingJoinableTasks; - syncObject = this.syncObject; - outstandingTasks = this.outstandingTasks; - taskFactory = this.taskFactory; - return joinableTaskFactory is object; - } + syncObject = this.syncObject; + outstandingTasks = this.outstandingTasks; + taskFactory = this.taskFactory; + return joinableTaskFactory is object; } } diff --git a/src/Microsoft.VisualStudio.Threading/Properties/AssemblyInfo.cs b/src/Microsoft.VisualStudio.Threading/Properties/AssemblyInfo.cs index 62399761c..a04c68ac9 100644 --- a/src/Microsoft.VisualStudio.Threading/Properties/AssemblyInfo.cs +++ b/src/Microsoft.VisualStudio.Threading/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; diff --git a/src/Microsoft.VisualStudio.Threading/README.md b/src/Microsoft.VisualStudio.Threading/README.md new file mode 100644 index 000000000..96928a56c --- /dev/null +++ b/src/Microsoft.VisualStudio.Threading/README.md @@ -0,0 +1,30 @@ +# Microsoft.VisualStudio.Threading + +Async synchronization primitives, async collections, TPL and dataflow extensions. The JoinableTaskFactory allows synchronously blocking the UI thread for async work. This package is applicable to any .NET application (not just Visual Studio). + +[Full documentation](https://microsoft.github.io/vs-threading/docs/getting-started.html). + +## Features + +* Async versions of many threading synchronization primitives + * `AsyncAutoResetEvent` + * `AsyncBarrier` + * `AsyncCountdownEvent` + * `AsyncManualResetEvent` + * `AsyncReaderWriterLock` + * `AsyncSemaphore` + * `ReentrantSemaphore` +* Async versions of very common types + * `AsyncEventHandler` + * `AsyncLazy` + * `AsyncLazyInitializer` + * `AsyncLocal` + * `AsyncQueue` +* Await extension methods + * Await on a `TaskScheduler` to switch to it. + Switch to a background thread with `await TaskScheduler.Default;` + * Await on a `Task` with a timeout + * Await on a `Task` with cancellation +* `JoinableTaskFactory` that allows you to schedule asynchronous or synchronous work + that does not deadlock with the UI thread even when the UI thread needs to + synchronously block on the result. diff --git a/src/Microsoft.VisualStudio.Threading/RarelyRemoveItemSet`1.cs b/src/Microsoft.VisualStudio.Threading/RarelyRemoveItemSet`1.cs new file mode 100644 index 000000000..429c321f4 --- /dev/null +++ b/src/Microsoft.VisualStudio.Threading/RarelyRemoveItemSet`1.cs @@ -0,0 +1,244 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// A collection optimized for usually small number of elements, and items are rarely removed. +/// Note: this implementation is not thread-safe. It must be protected to prevent race conditions. +/// +/// The type of elements to be stored. +internal struct RarelyRemoveItemSet + where T : class? +{ + private const int MaxExpansionSize = 16 * 1024; + + /// + /// The single value or array of values stored by this collection. + /// + private object? value; + + /// + /// The number of items. + /// + private int count; + + /// + /// Adds an element to the collection. + /// + internal void Add(T value) + { + if (this.value is T?[] valueArray) + { + if (valueArray.Length > this.count) + { + valueArray[this.count] = value; + } + else + { + int nextSize = valueArray.Length > MaxExpansionSize ? (valueArray.Length + MaxExpansionSize) : valueArray.Length * 2; + + Array.Resize(ref valueArray, nextSize); + valueArray[this.count] = value; + this.value = valueArray; + } + } + else + { + if (this.count == 0) + { + this.value = value; + } + else + { + Assumes.True(this.count == 1); + valueArray = new T?[2] { (T?)this.value, value }; + this.value = valueArray; + } + } + + this.count++; + } + + /// + /// Removes an element from the collection. + /// + internal void Remove(T value) + { + if (this.count == 0) + { + return; + } + + if (this.value is T?[] valueArray) + { + for (int i = 0; i < this.count; i++) + { + if (valueArray[i] == value) + { + // found matched item + --this.count; + + if (i < this.count) + { + // if the item removed was not the latest item in the array, we move the latest item in the original array there to fill the hole. + // After that, we reduce the size of the array by 1. (This eliminates extra work to move more than one item during Remove.) + valueArray[i] = valueArray[this.count]; + } + + // prevent holding reference + valueArray[this.count] = null; + break; + } + } + } + else if (this.value == value) + { + this.value = null; + this.count = 0; + } + } + + /// + /// Gets the result out of the current list, and reset it to empty. + /// + internal Enumerable EnumerateAndClear() + { + var copy = new Enumerable(this.value, this.count); + + this.value = null; + this.count = 0; + + return copy; + } + + /// + /// Make a thread safe copy of the content of this list. + /// + internal T[] ToArray() + { + if (this.count == 0) + { + return Array.Empty(); + } + + var results = new T[this.count]; + if (this.value is T?[] valueArray) + { + Array.Copy(valueArray, results, this.count); + } + else + { + results[0] = (T)this.value!; + } + + return results; + } + + internal struct Enumerator : IEnumerator + { + private const int IndexBeforeFirstArrayElement = -1; + private const int IndexSingleElement = -2; + private const int IndexBeforeSingleElement = -3; + + private readonly object? enumeratedValue; + private readonly int count; + + private int currentIndex; + + internal Enumerator(object? enumeratedValue, int count) + { + this.enumeratedValue = enumeratedValue; + this.count = count; + this.currentIndex = 0; + this.Reset(); + } + + public T Current + { + get + { + if (this.currentIndex >= 0 && this.currentIndex < this.count) + { + return ((T[])this.enumeratedValue!)[this.currentIndex]; + } + else if (this.currentIndex == IndexSingleElement) + { + return (T)this.enumeratedValue!; + } + + throw new InvalidOperationException(); + } + } + + object? System.Collections.IEnumerator.Current => this.Current; + + public void Dispose() + { + } + + public bool MoveNext() + { + if (this.currentIndex >= 0) + { + if (this.currentIndex < this.count) + { + this.currentIndex++; + return this.currentIndex < this.count; + } + } + else + { + switch (this.currentIndex) + { + case IndexBeforeSingleElement: + if (this.count > 0) + { + this.currentIndex = IndexSingleElement; + return true; + } + + break; + + case IndexBeforeFirstArrayElement: + this.currentIndex = 0; + return this.count > 0; + } + } + + return false; + } + + public void Reset() + { + this.currentIndex = this.enumeratedValue is T?[] ? IndexBeforeFirstArrayElement : IndexBeforeSingleElement; + } + } + + internal readonly struct Enumerable + { + private readonly object? value; + + /// + /// The number of items. + /// + private readonly int count; + + internal Enumerable(object? value, int count) + { + this.value = value; + this.count = count; + } + + /// + /// Returns an enumerator for a current snapshot of the collection. + /// + public Enumerator GetEnumerator() + { + return new Enumerator(this.value, this.count); + } + } +} diff --git a/src/Microsoft.VisualStudio.Threading/Reasons.cs b/src/Microsoft.VisualStudio.Threading/Reasons.cs new file mode 100644 index 000000000..8d8189e95 --- /dev/null +++ b/src/Microsoft.VisualStudio.Threading/Reasons.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.Threading; + +internal static class Reasons +{ + internal const string DiagnosticAnalysisOnly = "This API performs analysis of runtime objects for diagnostic purposes only, and isn't required for correct functionality."; +} diff --git a/src/Microsoft.VisualStudio.Threading/ReentrantSemaphore.cs b/src/Microsoft.VisualStudio.Threading/ReentrantSemaphore.cs index d6b97eb20..fbe46fe2d 100644 --- a/src/Microsoft.VisualStudio.Threading/ReentrantSemaphore.cs +++ b/src/Microsoft.VisualStudio.Threading/ReentrantSemaphore.cs @@ -1,403 +1,713 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// A -aware semaphore that allows reentrancy without consuming another slot in the semaphore. +/// +[DebuggerDisplay(nameof(CurrentCount) + " = {" + nameof(CurrentCount) + "}")] +public abstract class ReentrantSemaphore : IDisposable { - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Diagnostics.CodeAnalysis; - using System.Globalization; - using System.Runtime.CompilerServices; - using System.Threading; - using System.Threading.Tasks; + /// + /// The factory to wrap all pending and active semaphore requests with to mitigate deadlocks. + /// + private readonly JoinableTaskFactory? joinableTaskFactory; + + /// + /// The collection of all semaphore holders (and possibly waiters), which waiters should join to mitigate deadlocks. + /// + private readonly JoinableTaskCollection? joinableTaskCollection; + + /// + /// The underlying semaphore primitive. + /// + private readonly AsyncSemaphore semaphore; + + /// + /// Initializes a new instance of the class. + /// + /// The initial number of concurrent operations to allow. + /// The to use to mitigate deadlocks. + /// + /// This is private protected so that others cannot derive from this type but we can within the assembly. + /// + private protected ReentrantSemaphore(int initialCount, JoinableTaskContext? joinableTaskContext) + { + if (joinableTaskContext is object) + { + this.joinableTaskCollection = joinableTaskContext.CreateCollection(); + this.joinableTaskFactory = joinableTaskContext.CreateFactory(this.joinableTaskCollection); + } + + this.semaphore = new AsyncSemaphore(initialCount); + } /// - /// A -aware semaphore that allows reentrancy without consuming another slot in the semaphore. + /// Describes ways the may behave when a semaphore request is made in a context that is already in the semaphore. /// - [DebuggerDisplay(nameof(CurrentCount) + " = {" + nameof(CurrentCount) + "}")] - public abstract class ReentrantSemaphore : IDisposable + public enum ReentrancyMode { /// - /// The factory to wrap all pending and active semaphore requests with to mitigate deadlocks. + /// Reject all requests when the caller has already entered the semaphore + /// (and not yet exited) by throwing an . /// - private readonly JoinableTaskFactory? joinableTaskFactory; + /// + /// When reentrancy is not expected this is the recommended mode as it will prevent deadlocks + /// when unexpected reentrancy is detected. + /// + NotAllowed, /// - /// The collection of all semaphore holders (and possibly waiters), which waiters should join to mitigate deadlocks. + /// Each request occupies a unique slot in the semaphore. + /// Reentrancy is not recognized and may lead to deadlocks if the reentrancy level exceeds the count on the semaphore. + /// This resembles the behavior of the class. /// - private readonly JoinableTaskCollection? joinableTaskCollection; + /// + /// If reentrancy is not in the design, but leads to exceptions due to + /// ExecutionContext flowing unexpectedly, this mode may be the best option. + /// + NotRecognized, /// - /// The underlying semaphore primitive. + /// A request made by a caller that is already in the semaphore is immediately executed, + /// and shares the same semaphore slot with its parent. + /// This nested request must exit before its parent (Strict LIFO/stack behavior). + /// Exiting the semaphore before a child has or after the parent has will cause an + /// to fault the returned + /// from . /// - private readonly AsyncSemaphore semaphore; + /// + /// When reentrancy is a requirement, this mode helps ensure that reentrancy only happens + /// where code enters a semaphore, then awaits on other code that itself may enter the semaphore. + /// When a violation occurs, this semaphore transitions into a faulted state, after which any call + /// will throw an . + /// + Stack, /// - /// Initializes a new instance of the class. + /// A request made by a caller that is already in the semaphore is immediately executed, + /// and shares the same semaphore slot with its parent. + /// The slot is only released when all requests have exited, which may be in any order. /// - /// The initial number of concurrent operations to allow. - /// The to use to mitigate deadlocks. - /// - /// This is private protected so that others cannot derive from this type but we can within the assembly. - /// - private protected ReentrantSemaphore(int initialCount, JoinableTaskContext? joinableTaskContext) - { - if (joinableTaskContext is object) - { - this.joinableTaskCollection = joinableTaskContext.CreateCollection(); - this.joinableTaskFactory = joinableTaskContext.CreateFactory(this.joinableTaskCollection); - } + /// + /// This is the most permissive, but has the highest risk that leaked semaphore access will remain undetected. + /// Leaked semaphore access is a condition where code is inappropriately considered parented to another semaphore holder, + /// leading to it being allowed to run code within the semaphore, potentially in parallel with the actual semaphore holder. + /// + Freeform, + } - this.semaphore = new AsyncSemaphore(initialCount); + /// + /// Gets the number of openings that remain in the semaphore. + /// + public int CurrentCount + { + get + { + this.ThrowIfFaulted(); + return this.semaphore.CurrentCount; } + } - /// - /// Describes ways the may behave when a semaphore request is made in a context that is already in the semaphore. - /// - public enum ReentrancyMode + /// + /// Initializes a new instance of the class. + /// + /// The initial number of concurrent operations to allow. + /// The to use to mitigate deadlocks. + /// How to respond to a semaphore request by a caller that has already entered the semaphore. + public static ReentrantSemaphore Create(int initialCount = 1, JoinableTaskContext? joinableTaskContext = null, ReentrancyMode mode = ReentrancyMode.NotAllowed) + { + switch (mode) { - /// - /// Reject all requests when the caller has already entered the semaphore - /// (and not yet exited) by throwing an . - /// - /// - /// When reentrancy is not expected this is the recommended mode as it will prevent deadlocks - /// when unexpected reentrancy is detected. - /// - NotAllowed, - - /// - /// Each request occupies a unique slot in the semaphore. - /// Reentrancy is not recognized and may lead to deadlocks if the reentrancy level exceeds the count on the semaphore. - /// This resembles the behavior of the class. - /// - /// - /// If reentrancy is not in the design, but leads to exceptions due to - /// ExecutionContext flowing unexpectedly, this mode may be the best option. - /// - NotRecognized, - - /// - /// A request made by a caller that is already in the semaphore is immediately executed, - /// and shares the same semaphore slot with its parent. - /// This nested request must exit before its parent (Strict LIFO/stack behavior). - /// Exiting the semaphore before a child has or after the parent has will cause an - /// to fault the returned - /// from . - /// - /// - /// When reentrancy is a requirement, this mode helps ensure that reentrancy only happens - /// where code enters a semaphore, then awaits on other code that itself may enter the semaphore. - /// When a violation occurs, this semaphore transitions into a faulted state, after which any call - /// will throw an . - /// - Stack, - - /// - /// A request made by a caller that is already in the semaphore is immediately executed, - /// and shares the same semaphore slot with its parent. - /// The slot is only released when all requests have exited, which may be in any order. - /// - /// - /// This is the most permissive, but has the highest risk that leaked semaphore access will remain undetected. - /// Leaked semaphore access is a condition where code is inappropriately considered parented to another semaphore holder, - /// leading to it being allowed to run code within the semaphore, potentially in parallel with the actual semaphore holder. - /// - Freeform, + case ReentrancyMode.NotRecognized: + return new NotRecognizedSemaphore(initialCount, joinableTaskContext); + case ReentrancyMode.NotAllowed: + return new NotAllowedSemaphore(initialCount, joinableTaskContext); + case ReentrancyMode.Stack: + return new StackSemaphore(initialCount, joinableTaskContext); + case ReentrancyMode.Freeform: + return new FreeformSemaphore(initialCount, joinableTaskContext); + default: + throw new ArgumentOutOfRangeException(nameof(mode)); } + } - /// - /// Gets the number of openings that remain in the semaphore. - /// - public int CurrentCount + /// + /// Executes a given operation within the semaphore. + /// + /// + /// The delegate to invoke once the semaphore is entered. If a was supplied to the constructor, + /// this delegate will execute on the main thread if this is invoked on the main thread, otherwise it will be invoked on the + /// threadpool. When no is supplied to the constructor, this delegate will execute on the + /// caller's context. + /// + /// A cancellation token. + /// A task that completes with the result of , after the semaphore has been exited. + /// + public abstract Task ExecuteAsync(Func operation, CancellationToken cancellationToken = default); + + /// + /// Executes a given operation within the semaphore. + /// + /// The type of value returned by the operation. + /// + /// The delegate to invoke once the semaphore is entered. If a was supplied to the constructor, + /// this delegate will execute on the main thread if this is invoked on the main thread, otherwise it will be invoked on the + /// threadpool. When no is supplied to the constructor, this delegate will execute on the + /// caller's context. + /// + /// A cancellation token. + /// A task that completes with the result of , after the semaphore has been exited. + /// + /// Thrown when reentrancy is detected and not allowed based due to being provided to the constructor. + /// This happens when code that already holds the semaphore calls code that attempts to again enter the semaphore. + /// When the called code is not awaited on by the caller, it may be appropriate to suppress this reentrancy detection for the method + /// that is called in a fire-and-forget fashion. + /// To suppress this exception for that specific case while preserving overall protection, use . + /// + public abstract ValueTask ExecuteAsync(Func> operation, CancellationToken cancellationToken = default); + + /// + /// Conceals evidence that the caller has entered this till its result is disposed. + /// + /// A value to dispose to restore visibility of any presence in this semaphore. + /// + /// This method is useful when the caller is about to spin off another operation (e.g. scheduling work to the threadpool) + /// that it does not consider vital to its own completion, in order to prevent the spun off work from abusing the + /// caller's right to the semaphore. + /// This is a safe call to make whether or not the semaphore is currently held, or whether reentrancy is allowed on this instance. + /// + /// + /// + /// The following snippet demonstrates a way to use this method. + /// + /// + /// + public virtual RevertRelevance SuppressRelevance() => default; + + /// + /// Faults all pending semaphore waiters with + /// and rejects all subsequent attempts to enter the semaphore with the same exception. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes managed and unmanaged resources held by this instance. + /// + /// if was called; if the object is being finalized. + protected virtual void Dispose(bool disposing) + { + if (disposing) { - get - { - this.ThrowIfFaulted(); - return this.semaphore.CurrentCount; - } + this.semaphore.Dispose(); } + } - /// - /// Initializes a new instance of the class. - /// - /// The initial number of concurrent operations to allow. - /// The to use to mitigate deadlocks. - /// How to respond to a semaphore request by a caller that has already entered the semaphore. - public static ReentrantSemaphore Create(int initialCount = 1, JoinableTaskContext? joinableTaskContext = null, ReentrancyMode mode = ReentrancyMode.NotAllowed) + /// + /// Throws an exception if this instance has been faulted. + /// + protected virtual void ThrowIfFaulted() + { + } + + /// + /// Disposes the specfied release, swallowing certain exceptions. + /// + /// The releaser to dispose. + private static void DisposeReleaserNoThrow(AsyncSemaphore.Releaser releaser) + { + try { - switch (mode) - { - case ReentrancyMode.NotRecognized: - return new NotRecognizedSemaphore(initialCount, joinableTaskContext); - case ReentrancyMode.NotAllowed: - return new NotAllowedSemaphore(initialCount, joinableTaskContext); - case ReentrancyMode.Stack: - return new StackSemaphore(initialCount, joinableTaskContext); - case ReentrancyMode.Freeform: - return new FreeformSemaphore(initialCount, joinableTaskContext); - default: - throw new ArgumentOutOfRangeException(nameof(mode)); - } + releaser.Dispose(); + } + catch (ObjectDisposedException) + { + // Swallow this, since in releasing the semaphore if it's already disposed the caller probably doesn't care. } + } + + /// + /// Gets a value indicating whether this instance is using Joinable Task aware or not. + /// + private bool IsJoinableTaskAware([NotNullWhen(true)] out JoinableTaskFactory? joinableTaskFactory, [NotNullWhen(true)] out JoinableTaskCollection? joinableTaskCollection) + { + joinableTaskFactory = this.joinableTaskFactory; + joinableTaskCollection = this.joinableTaskCollection; + return this.joinableTaskCollection is object; + } + + /// + /// Executes the semaphore request. + /// + /// The delegate that requests the semaphore and executes code within it. + /// A value for the caller to await on. + private AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable ExecuteCoreAsync(Func semaphoreUser) + { + Requires.NotNull(semaphoreUser, nameof(semaphoreUser)); + + return this.joinableTaskFactory is object + ? this.joinableTaskFactory.RunAsync(semaphoreUser).Task.ConfigureAwaitRunInline() + : semaphoreUser().ConfigureAwaitRunInline(); + } + /// + /// Executes the semaphore request. + /// + /// The delegate that requests the semaphore and executes code within it. + /// A value for the caller to await on. + private AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable ExecuteCoreAsync(Func> semaphoreUser) + { + Requires.NotNull(semaphoreUser, nameof(semaphoreUser)); + + return this.joinableTaskFactory is object + ? this.joinableTaskFactory.RunAsync(semaphoreUser).Task.ConfigureAwaitRunInline() + : semaphoreUser().ConfigureAwaitRunInline(); + } + + /// + /// A structure that hides any evidence that the caller has entered a till this value is disposed. + /// + public readonly struct RevertRelevance : IDisposable + { /// - /// Executes a given operation within the semaphore. + /// The delegate to invoke on disposal. /// - /// - /// The delegate to invoke once the semaphore is entered. If a was supplied to the constructor, - /// this delegate will execute on the main thread if this is invoked on the main thread, otherwise it will be invoked on the - /// threadpool. When no is supplied to the constructor, this delegate will execute on the - /// caller's context. - /// - /// A cancellation token. - /// A task that completes with the result of , after the semaphore has been exited. - public abstract Task ExecuteAsync(Func operation, CancellationToken cancellationToken = default); + private readonly Action disposeAction; /// - /// Executes a given operation within the semaphore. + /// The instance that is suppressing relevance. /// - /// The type of value returned by the operation. - /// - /// The delegate to invoke once the semaphore is entered. If a was supplied to the constructor, - /// this delegate will execute on the main thread if this is invoked on the main thread, otherwise it will be invoked on the - /// threadpool. When no is supplied to the constructor, this delegate will execute on the - /// caller's context. - /// - /// A cancellation token. - /// A task that completes with the result of , after the semaphore has been exited. - public abstract ValueTask ExecuteAsync(Func> operation, CancellationToken cancellationToken = default); + private readonly ReentrantSemaphore semaphore; /// - /// Conceals evidence that the caller has entered this till its result is disposed. + /// The argument to pass to the delegate. /// - /// A value to dispose to restore visibility of any presence in this semaphore. - /// - /// This method is useful when the caller is about to spin off another operation (e.g. scheduling work to the threadpool) - /// that it does not consider vital to its own completion, in order to prevent the spun off work from abusing the - /// caller's right to the semaphore. - /// This is a safe call to make whether or not the semaphore is currently held, or whether reentrancy is allowed on this instance. - /// - public virtual RevertRelevance SuppressRelevance() => default; + private readonly object? state; /// - /// Faults all pending semaphore waiters with - /// and rejects all subsequent attempts to enter the semaphore with the same exception. + /// Initializes a new instance of the struct. /// - public void Dispose() + /// The delegate to invoke on disposal. + /// The instance that is suppressing relevance. + /// The argument to pass to the delegate. + internal RevertRelevance(Action disposeAction, ReentrantSemaphore semaphore, object? state) { - this.Dispose(true); - GC.SuppressFinalize(this); + this.disposeAction = disposeAction; + this.semaphore = semaphore; + this.state = state; } + /// + public void Dispose() => this.disposeAction?.Invoke(this.semaphore, this.state); + } + + /// + /// An implementation of supporting the mode. + /// + private class NotRecognizedSemaphore : ReentrantSemaphore + { /// - /// Disposes managed and unmanaged resources held by this instance. + /// Initializes a new instance of the class. /// - /// true if was called; false if the object is being finalized. - protected virtual void Dispose(bool disposing) + /// The initial number of concurrent operations to allow. + /// The to use to mitigate deadlocks. + internal NotRecognizedSemaphore(int initialCount, JoinableTaskContext? joinableTaskContext) + : base(initialCount, joinableTaskContext) { - if (disposing) - { - this.semaphore.Dispose(); - } } - /// - /// Throws an exception if this instance has been faulted. - /// - protected virtual void ThrowIfFaulted() + /// + public override async Task ExecuteAsync(Func operation, CancellationToken cancellationToken = default) { + Requires.NotNull(operation, nameof(operation)); + + // Note: this code is duplicated and not extracted to minimize allocating extra async state machines. + // For performance reasons in the JTF enabled scenario, we want to minimize the number of Joins performed, and also + // keep the size of the JoinableCollection to a minimum. This also means awaiting on the semaphore outside of a + // JTF.RunAsync. This requires us to not ConfigureAwait(true) on the semaphore. However, that prevents us from + // resuming on the correct sync context. To partially fix this, we will at least resume you on the main thread or + // thread pool. + AsyncSemaphore.Releaser releaser = default; + try + { + bool resumeOnMainThread = this.IsJoinableTaskAware(out _, out JoinableTaskCollection? joinableTaskCollection) + ? joinableTaskCollection.Context.IsOnMainThread + : false; + bool mustYield = false; + using (this.joinableTaskCollection?.Join()) + { + if (this.IsJoinableTaskAware(out _, out _)) + { + // Use ConfiguredAwaitRunInline() as ConfigureAwait(true) will + // deadlock due to not being inside a JTF.RunAsync(). + Task? releaserTask = this.semaphore.EnterAsync(cancellationToken); + mustYield = !releaserTask.IsCompleted; + releaser = await releaserTask.ConfigureAwaitRunInline(); + } + else + { + releaser = await this.semaphore.EnterAsync(cancellationToken).ConfigureAwait(true); + } + } + + await this.ExecuteCoreAsync(async delegate + { + if (this.IsJoinableTaskAware(out JoinableTaskFactory? joinableTaskFactory, out _)) + { + if (resumeOnMainThread) + { + // Return to the main thread if we started there. + await joinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + } + else + { + await TaskScheduler.Default; + } + + if (mustYield) + { + // Yield to prevent running on the stack that released the semaphore. + await Task.Yield(); + } + } + + await operation().ConfigureAwaitRunInline(); + }); + } + finally + { + DisposeReleaserNoThrow(releaser); + } } - /// - /// Disposes the specfied release, swallowing certain exceptions. - /// - /// The releaser to dispose. - private static void DisposeReleaserNoThrow(AsyncSemaphore.Releaser releaser) + /// + public override async ValueTask ExecuteAsync(Func> operation, CancellationToken cancellationToken = default) { + Requires.NotNull(operation, nameof(operation)); + + // Note: this code is duplicated and not extracted to minimize allocating extra async state machines. + // For performance reasons in the JTF enabled scenario, we want to minimize the number of Joins performed, and also + // keep the size of the JoinableCollection to a minimum. This also means awaiting on the semaphore outside of a + // JTF.RunAsync. This requires us to not ConfigureAwait(true) on the semaphore. However, that prevents us from + // resuming on the correct sync context. To partially fix this, we will at least resume you on the main thread or + // thread pool. + AsyncSemaphore.Releaser releaser = default; try { - releaser.Dispose(); + bool resumeOnMainThread = this.IsJoinableTaskAware(out _, out JoinableTaskCollection? joinableTaskCollection) + ? joinableTaskCollection.Context.IsOnMainThread + : false; + bool mustYield = false; + using (this.joinableTaskCollection?.Join()) + { + if (this.IsJoinableTaskAware(out _, out _)) + { + // Use ConfiguredAwaitRunInline() as ConfigureAwait(true) will + // deadlock due to not being inside a JTF.RunAsync(). + Task? releaserTask = this.semaphore.EnterAsync(cancellationToken); + + // Yield to prevent running on the stack that released the semaphore. + mustYield = !releaserTask.IsCompleted; + + releaser = await releaserTask.ConfigureAwaitRunInline(); + } + else + { + releaser = await this.semaphore.EnterAsync(cancellationToken).ConfigureAwait(true); + } + } + + return await this.ExecuteCoreAsync(async delegate + { + if (this.IsJoinableTaskAware(out JoinableTaskFactory? joinableTaskFactory, out _)) + { + if (resumeOnMainThread) + { + // Return to the main thread if we started there. + await joinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: mustYield, cancellationToken); + } + else + { + await TaskScheduler.Default.SwitchTo(alwaysYield: mustYield); + } + } + + return await operation().ConfigureAwait(true); + }); } - catch (ObjectDisposedException) + finally { - // Swallow this, since in releasing the semaphore if it's already disposed the caller probably doesn't care. + DisposeReleaserNoThrow(releaser); } } + } + /// + /// An implementation of supporting the mode. + /// + private class NotAllowedSemaphore : ReentrantSemaphore + { /// - /// Gets a value indicating whether this instance is using Joinable Task aware or not. + /// The means to recognize that a caller has already entered the semaphore. /// - private bool IsJoinableTaskAware([NotNullWhen(true)] out JoinableTaskFactory? joinableTaskFactory, [NotNullWhen(true)] out JoinableTaskCollection? joinableTaskCollection) - { - joinableTaskFactory = this.joinableTaskFactory; - joinableTaskCollection = this.joinableTaskCollection; - return this.joinableTaskCollection is object; - } + /// + /// We use instead of just here for two reasons: + /// 1. Our own class requires a ref type for the generic type argument. + /// 2. (more importantly) we need all forks of an ExecutionContext to observe updates to the value. + /// But ExecutionContext is copy-on-write so forks don't see changes to it. + /// lets us store and later update the boxed value of the existing box reference. + /// + private readonly AsyncLocal> reentrancyDetection = new AsyncLocal>(); /// - /// Executes the semaphore request. + /// Initializes a new instance of the class. /// - /// The delegate that requests the semaphore and executes code within it. - /// A value for the caller to await on. - private AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable ExecuteCoreAsync(Func semaphoreUser) + /// The initial number of concurrent operations to allow. + /// The to use to mitigate deadlocks. + internal NotAllowedSemaphore(int initialCount, JoinableTaskContext? joinableTaskContext) + : base(initialCount, joinableTaskContext) { - Requires.NotNull(semaphoreUser, nameof(semaphoreUser)); - - return this.joinableTaskFactory is object - ? this.joinableTaskFactory.RunAsync(semaphoreUser).Task.ConfigureAwaitRunInline() - : semaphoreUser().ConfigureAwaitRunInline(); } - /// - /// Executes the semaphore request. - /// - /// The delegate that requests the semaphore and executes code within it. - /// A value for the caller to await on. - private AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable ExecuteCoreAsync(Func> semaphoreUser) + /// + public override async Task ExecuteAsync(Func operation, CancellationToken cancellationToken = default) { - Requires.NotNull(semaphoreUser, nameof(semaphoreUser)); + Requires.NotNull(operation, nameof(operation)); + this.ThrowIfFaulted(); - return this.joinableTaskFactory is object - ? this.joinableTaskFactory.RunAsync(semaphoreUser).Task.ConfigureAwaitRunInline() - : semaphoreUser().ConfigureAwaitRunInline(); - } + StrongBox? ownedBox = this.reentrancyDetection.Value; + if (ownedBox?.Value ?? false) + { + throw Verify.FailOperation(Strings.SemaphoreAlreadyHeld, ReentrancyMode.NotAllowed); + } - /// - /// A structure that hides any evidence that the caller has entered a till this value is disposed. - /// - public readonly struct RevertRelevance : IDisposable - { - /// - /// The delegate to invoke on disposal. - /// - private readonly Action disposeAction; - - /// - /// The instance that is suppressing relevance. - /// - private readonly ReentrantSemaphore semaphore; - - /// - /// The argument to pass to the delegate. - /// - private readonly object? state; - - /// - /// Initializes a new instance of the struct. - /// - /// The delegate to invoke on disposal. - /// The instance that is suppressing relevance. - /// The argument to pass to the delegate. - internal RevertRelevance(Action disposeAction, ReentrantSemaphore semaphore, object? state) + // Note: this code is duplicated and not extracted to minimize allocating extra async state machines. + // For performance reasons in the JTF enabled scenario, we want to minimize the number of Joins performed, and also + // keep the size of the JoinableCollection to a minimum. This also means awaiting on the semaphore outside of a + // JTF.RunAsync. This requires us to not ConfigureAwait(true) on the semaphore. However, that prevents us from + // resuming on the correct sync context. To partially fix this, we will at least resume you on the main thread or + // thread pool. + AsyncSemaphore.Releaser releaser = default; + try { - this.disposeAction = disposeAction; - this.semaphore = semaphore; - this.state = state; + bool resumeOnMainThread = this.IsJoinableTaskAware(out _, out JoinableTaskCollection? joinableTaskCollection) + ? joinableTaskCollection.Context.IsOnMainThread + : false; + bool mustYield = false; + using (this.joinableTaskCollection?.Join()) + { + if (this.IsJoinableTaskAware(out _, out _)) + { + // Use ConfiguredAwaitRunInline() as ConfigureAwait(true) will + // deadlock due to not being inside a JTF.RunAsync(). + Task? releaserTask = this.semaphore.EnterAsync(cancellationToken); + + // Yield to prevent running on the stack that released the semaphore. + mustYield = !releaserTask.IsCompleted; + + releaser = await releaserTask.ConfigureAwaitRunInline(); + } + else + { + releaser = await this.semaphore.EnterAsync(cancellationToken).ConfigureAwait(true); + } + } + + await this.ExecuteCoreAsync(async delegate + { + if (this.IsJoinableTaskAware(out JoinableTaskFactory? joinableTaskFactory, out _)) + { + if (resumeOnMainThread) + { + // Return to the main thread if we started there. + await joinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: mustYield, cancellationToken); + } + else + { + await TaskScheduler.Default.SwitchTo(alwaysYield: mustYield); + } + } + + this.reentrancyDetection.Value = ownedBox = new StrongBox(true); + await operation().ConfigureAwaitRunInline(); + }); } + finally + { + // Make it clear to any forks of our ExecutionContexxt that the semaphore is no longer owned. + // Null check incase the switch to UI thread was cancelled. + if (ownedBox is object) + { + ownedBox.Value = false; + } - /// - public void Dispose() => this.disposeAction?.Invoke(this.semaphore, this.state); + DisposeReleaserNoThrow(releaser); + } } - /// - /// An implementation of supporting the mode. - /// - private class NotRecognizedSemaphore : ReentrantSemaphore + /// + public override async ValueTask ExecuteAsync(Func> operation, CancellationToken cancellationToken = default) { - /// - /// Initializes a new instance of the class. - /// - /// The initial number of concurrent operations to allow. - /// The to use to mitigate deadlocks. - internal NotRecognizedSemaphore(int initialCount, JoinableTaskContext? joinableTaskContext) - : base(initialCount, joinableTaskContext) + Requires.NotNull(operation, nameof(operation)); + this.ThrowIfFaulted(); + + StrongBox? ownedBox = this.reentrancyDetection.Value; + if (ownedBox?.Value ?? false) { + throw Verify.FailOperation(Strings.SemaphoreAlreadyHeld, ReentrancyMode.NotAllowed); } - /// - public override async Task ExecuteAsync(Func operation, CancellationToken cancellationToken = default) + // Note: this code is duplicated and not extracted to minimize allocating extra async state machines. + // For performance reasons in the JTF enabled scenario, we want to minimize the number of Joins performed, and also + // keep the size of the JoinableCollection to a minimum. This also means awaiting on the semaphore outside of a + // JTF.RunAsync. This requires us to not ConfigureAwait(true) on the semaphore. However, that prevents us from + // resuming on the correct sync context. To partially fix this, we will at least resume you on the main thread or + // thread pool. + AsyncSemaphore.Releaser releaser = default; + try { - Requires.NotNull(operation, nameof(operation)); - - // Note: this code is duplicated and not extracted to minimize allocating extra async state machines. - // For performance reasons in the JTF enabled scenario, we want to minimize the number of Joins performed, and also - // keep the size of the JoinableCollection to a minimum. This also means awaiting on the semaphore outside of a - // JTF.RunAsync. This requires us to not ConfigureAwait(true) on the semaphore. However, that prevents us from - // resuming on the correct sync context. To partially fix this, we will at least resume you on the main thread or - // thread pool. - AsyncSemaphore.Releaser releaser = default; - try + bool resumeOnMainThread = this.IsJoinableTaskAware(out _, out JoinableTaskCollection? joinableTaskCollection) + ? joinableTaskCollection.Context.IsOnMainThread + : false; + bool mustYield = false; + using (this.joinableTaskCollection?.Join()) { - bool resumeOnMainThread = this.IsJoinableTaskAware(out _, out JoinableTaskCollection? joinableTaskCollection) - ? joinableTaskCollection.Context.IsOnMainThread - : false; - bool mustYield = false; - using (this.joinableTaskCollection?.Join()) + if (this.IsJoinableTaskAware(out _, out _)) { - if (this.IsJoinableTaskAware(out _, out _)) + // Use ConfiguredAwaitRunInline() as ConfigureAwait(true) will + // deadlock due to not being inside a JTF.RunAsync(). + Task? releaserTask = this.semaphore.EnterAsync(cancellationToken); + + // Yield to prevent running on the stack that released the semaphore. + mustYield = !releaserTask.IsCompleted; + releaser = await releaserTask.ConfigureAwaitRunInline(); + } + else + { + releaser = await this.semaphore.EnterAsync(cancellationToken).ConfigureAwait(true); + } + } + + return await this.ExecuteCoreAsync(async delegate + { + if (this.IsJoinableTaskAware(out JoinableTaskFactory? joinableTaskFactory, out _)) + { + if (resumeOnMainThread) { - // Use ConfiguredAwaitRunInline() as ConfigureAwait(true) will - // deadlock due to not being inside a JTF.RunAsync(). - Task? releaserTask = this.semaphore.EnterAsync(cancellationToken); - mustYield = !releaserTask.IsCompleted; - releaser = await releaserTask.ConfigureAwaitRunInline(); + // Return to the main thread if we started there. + await joinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: mustYield, cancellationToken); } else { - releaser = await this.semaphore.EnterAsync(cancellationToken).ConfigureAwait(true); + await TaskScheduler.Default.SwitchTo(alwaysYield: mustYield); } } - await this.ExecuteCoreAsync(async delegate - { - if (this.IsJoinableTaskAware(out JoinableTaskFactory? joinableTaskFactory, out _)) - { - if (resumeOnMainThread) - { - // Return to the main thread if we started there. - await joinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - } - else - { - await TaskScheduler.Default; - } - - if (mustYield) - { - // Yield to prevent running on the stack that released the semaphore. - await Task.Yield(); - } - } - - await operation().ConfigureAwaitRunInline(); - }); - } - finally + this.reentrancyDetection.Value = ownedBox = new StrongBox(true); + return await operation().ConfigureAwait(true); + }); + } + finally + { + // Make it clear to any forks of our ExecutionContexxt that the semaphore is no longer owned. + // Null check incase the switch to UI thread was cancelled. + if (ownedBox is object) { - DisposeReleaserNoThrow(releaser); + ownedBox.Value = false; } + + DisposeReleaserNoThrow(releaser); } + } + + /// + public override RevertRelevance SuppressRelevance() + { + StrongBox? originalValue = this.reentrancyDetection.Value; + this.reentrancyDetection.Value = null; + return new RevertRelevance((t, s) => ((NotAllowedSemaphore)t).reentrancyDetection.Value = (StrongBox?)s, this, originalValue); + } + } - /// - public override async ValueTask ExecuteAsync(Func> operation, CancellationToken cancellationToken = default) + /// + /// An implementation of supporting the mode. + /// + private class StackSemaphore : ReentrantSemaphore + { + /// + /// The means to recognize that a caller has already entered the semaphore. + /// + /// + /// We use instead of just here + /// so that we have a unique identity for each Releaser that we can recognize as a means to verify + /// the integrity of the "stack" of semaphore reentrant requests. + /// + private readonly AsyncLocal>> reentrantCount = new AsyncLocal>>(); + + /// + /// A flag to indicate this instance was misused and the data it protects should not be touched as it may be corrupted. + /// + private bool faulted; + + /// + /// Initializes a new instance of the class. + /// + /// The initial number of concurrent operations to allow. + /// The to use to mitigate deadlocks. + internal StackSemaphore(int initialCount, JoinableTaskContext? joinableTaskContext) + : base(initialCount, joinableTaskContext) + { + } + + /// + public override async Task ExecuteAsync(Func operation, CancellationToken cancellationToken = default) + { + Requires.NotNull(operation, nameof(operation)); + this.ThrowIfFaulted(); + + // No race condition here: We're accessing AsyncLocal which we by definition have our own copy of. + // Multiple threads or multiple async methods will all have their own storage for this field. + Stack>? reentrantStack = this.reentrantCount.Value; + if (reentrantStack is null || reentrantStack.Count == 0) { - Requires.NotNull(operation, nameof(operation)); - - // Note: this code is duplicated and not extracted to minimize allocating extra async state machines. - // For performance reasons in the JTF enabled scenario, we want to minimize the number of Joins performed, and also - // keep the size of the JoinableCollection to a minimum. This also means awaiting on the semaphore outside of a - // JTF.RunAsync. This requires us to not ConfigureAwait(true) on the semaphore. However, that prevents us from - // resuming on the correct sync context. To partially fix this, we will at least resume you on the main thread or - // thread pool. - AsyncSemaphore.Releaser releaser = default; - try + // When the stack is empty, the semaphore isn't held. But many execution contexts that forked from a common root + // would be sharing this same empty Stack instance. If we pushed to that Stack, all those forks would suddenly + // be seen as having entered this new top-level semaphore. We therefore allocate a new Stack and assign it to our + // AsyncLocal field so that only this particular ExecutionContext is seen as having entered the semaphore. + this.reentrantCount.Value = reentrantStack = new Stack>(capacity: 2); + } + + // Note: this code is duplicated and not extracted to minimize allocating extra async state machines. + // For performance reasons in the JTF enabled scenario, we want to minimize the number of Joins performed, and also + // keep the size of the JoinableCollection to a minimum. This also means awaiting on the semaphore outside of a + // JTF.RunAsync. This requires us to not ConfigureAwait(true) on the semaphore. However, that prevents us from + // resuming on the correct sync context. To partially fix this, we will at least resume you on the main thread or + // thread pool. + AsyncSemaphore.Releaser releaser = default; + bool pushed = false; + StrongBox? pushedReleaser = null; + try + { + bool resumeOnMainThread = this.IsJoinableTaskAware(out _, out JoinableTaskCollection? joinableTaskCollection) + ? joinableTaskCollection.Context.IsOnMainThread + : false; + bool mustYield = false; + if (reentrantStack.Count == 0) { - bool resumeOnMainThread = this.IsJoinableTaskAware(out _, out JoinableTaskCollection? joinableTaskCollection) - ? joinableTaskCollection.Context.IsOnMainThread - : false; - bool mustYield = false; using (this.joinableTaskCollection?.Join()) { if (this.IsJoinableTaskAware(out _, out _)) @@ -416,24 +726,59 @@ public override async ValueTask ExecuteAsync(Func> operation, releaser = await this.semaphore.EnterAsync(cancellationToken).ConfigureAwait(true); } } + } + else + { + releaser = default; + } - return await this.ExecuteCoreAsync(async delegate + pushedReleaser = new StrongBox(releaser); + await this.ExecuteCoreAsync(async delegate + { + if (this.IsJoinableTaskAware(out JoinableTaskFactory? joinableTaskFactory, out _)) { - if (this.IsJoinableTaskAware(out JoinableTaskFactory? joinableTaskFactory, out _)) + if (resumeOnMainThread) { - if (resumeOnMainThread) - { - // Return to the main thread if we started there. - await joinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: mustYield, cancellationToken); - } - else + // Return to the main thread if we started there. + await joinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: mustYield, cancellationToken); + } + else + { + await TaskScheduler.Default.SwitchTo(alwaysYield: mustYield); + } + } + + // The semaphore faulted while we were waiting on it. + this.ThrowIfFaulted(); + + lock (reentrantStack) + { + reentrantStack.Push(pushedReleaser); + pushed = true; + } + + await operation().ConfigureAwaitRunInline(); + }); + } + finally + { + try + { + if (pushed) + { + lock (reentrantStack) + { + StrongBox? poppedReleaser = reentrantStack.Pop(); + if (!object.ReferenceEquals(poppedReleaser, pushedReleaser)) { - await TaskScheduler.Default.SwitchTo(alwaysYield: mustYield); + // When the semaphore faults, we will drain and throw for awaiting tasks one by one. + this.faulted = true; +#pragma warning disable CA2219 // Do not raise exceptions in finally clauses + throw new IllegalSemaphoreUsageException(string.Format(CultureInfo.CurrentCulture, Strings.SemaphoreStackNestingViolated, ReentrantSemaphore.ReentrancyMode.Stack)); +#pragma warning restore CA2219 // Do not raise exceptions in finally clauses } } - - return await operation().ConfigureAwait(true); - }); + } } finally { @@ -442,58 +787,41 @@ public override async ValueTask ExecuteAsync(Func> operation, } } - /// - /// An implementation of supporting the mode. - /// - private class NotAllowedSemaphore : ReentrantSemaphore + /// + public override async ValueTask ExecuteAsync(Func> operation, CancellationToken cancellationToken = default) { - /// - /// The means to recognize that a caller has already entered the semaphore. - /// - /// - /// We use instead of just here for two reasons: - /// 1. Our own class requires a ref type for the generic type argument. - /// 2. (more importantly) we need all forks of an ExecutionContext to observe updates to the value. - /// But ExecutionContext is copy-on-write so forks don't see changes to it. - /// lets us store and later update the boxed value of the existing box reference. - /// - private readonly AsyncLocal> reentrancyDetection = new AsyncLocal>(); - - /// - /// Initializes a new instance of the class. - /// - /// The initial number of concurrent operations to allow. - /// The to use to mitigate deadlocks. - internal NotAllowedSemaphore(int initialCount, JoinableTaskContext? joinableTaskContext) - : base(initialCount, joinableTaskContext) + Requires.NotNull(operation, nameof(operation)); + this.ThrowIfFaulted(); + + // No race condition here: We're accessing AsyncLocal which we by definition have our own copy of. + // Multiple threads or multiple async methods will all have their own storage for this field. + Stack>? reentrantStack = this.reentrantCount.Value; + if (reentrantStack is null || reentrantStack.Count == 0) { + // When the stack is empty, the semaphore isn't held. But many execution contexts that forked from a common root + // would be sharing this same empty Stack instance. If we pushed to that Stack, all those forks would suddenly + // be seen as having entered this new top-level semaphore. We therefore allocate a new Stack and assign it to our + // AsyncLocal field so that only this particular ExecutionContext is seen as having entered the semaphore. + this.reentrantCount.Value = reentrantStack = new Stack>(capacity: 2); } - /// - public override async Task ExecuteAsync(Func operation, CancellationToken cancellationToken = default) + // Note: this code is duplicated and not extracted to minimize allocating extra async state machines. + // For performance reasons in the JTF enabled scenario, we want to minimize the number of Joins performed, and also + // keep the size of the JoinableCollection to a minimum. This also means awaiting on the semaphore outside of a + // JTF.RunAsync. This requires us to not ConfigureAwait(true) on the semaphore. However, that prevents us from + // resuming on the correct sync context. To partially fix this, we will at least resume you on the main thread or + // thread pool. + AsyncSemaphore.Releaser releaser = default; + bool pushed = false; + StrongBox? pushedReleaser = null; + try { - Requires.NotNull(operation, nameof(operation)); - this.ThrowIfFaulted(); - - StrongBox? ownedBox = this.reentrancyDetection.Value; - if (ownedBox?.Value ?? false) - { - throw Verify.FailOperation(Strings.SemaphoreAlreadyHeld, ReentrancyMode.NotAllowed); - } - - // Note: this code is duplicated and not extracted to minimize allocating extra async state machines. - // For performance reasons in the JTF enabled scenario, we want to minimize the number of Joins performed, and also - // keep the size of the JoinableCollection to a minimum. This also means awaiting on the semaphore outside of a - // JTF.RunAsync. This requires us to not ConfigureAwait(true) on the semaphore. However, that prevents us from - // resuming on the correct sync context. To partially fix this, we will at least resume you on the main thread or - // thread pool. - AsyncSemaphore.Releaser releaser = default; - try + bool resumeOnMainThread = this.IsJoinableTaskAware(out _, out JoinableTaskCollection? joinableTaskCollection) + ? joinableTaskCollection.Context.IsOnMainThread + : false; + bool mustYield = false; + if (reentrantStack.Count == 0) { - bool resumeOnMainThread = this.IsJoinableTaskAware(out _, out JoinableTaskCollection? joinableTaskCollection) - ? joinableTaskCollection.Context.IsOnMainThread - : false; - bool mustYield = false; using (this.joinableTaskCollection?.Join()) { if (this.IsJoinableTaskAware(out _, out _)) @@ -512,614 +840,299 @@ public override async Task ExecuteAsync(Func operation, CancellationToken releaser = await this.semaphore.EnterAsync(cancellationToken).ConfigureAwait(true); } } - - await this.ExecuteCoreAsync(async delegate - { - if (this.IsJoinableTaskAware(out JoinableTaskFactory? joinableTaskFactory, out _)) - { - if (resumeOnMainThread) - { - // Return to the main thread if we started there. - await joinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: mustYield, cancellationToken); - } - else - { - await TaskScheduler.Default.SwitchTo(alwaysYield: mustYield); - } - } - - this.reentrancyDetection.Value = ownedBox = new StrongBox(true); - await operation().ConfigureAwaitRunInline(); - }); - } - finally - { - // Make it clear to any forks of our ExecutionContexxt that the semaphore is no longer owned. - // Null check incase the switch to UI thread was cancelled. - if (ownedBox is object) - { - ownedBox.Value = false; - } - - DisposeReleaserNoThrow(releaser); } - } - - /// - public override async ValueTask ExecuteAsync(Func> operation, CancellationToken cancellationToken = default) - { - Requires.NotNull(operation, nameof(operation)); - this.ThrowIfFaulted(); - - StrongBox? ownedBox = this.reentrancyDetection.Value; - if (ownedBox?.Value ?? false) + else { - throw Verify.FailOperation(Strings.SemaphoreAlreadyHeld, ReentrancyMode.NotAllowed); + releaser = default; } - // Note: this code is duplicated and not extracted to minimize allocating extra async state machines. - // For performance reasons in the JTF enabled scenario, we want to minimize the number of Joins performed, and also - // keep the size of the JoinableCollection to a minimum. This also means awaiting on the semaphore outside of a - // JTF.RunAsync. This requires us to not ConfigureAwait(true) on the semaphore. However, that prevents us from - // resuming on the correct sync context. To partially fix this, we will at least resume you on the main thread or - // thread pool. - AsyncSemaphore.Releaser releaser = default; - try + pushedReleaser = new StrongBox(releaser); + return await this.ExecuteCoreAsync(async delegate { - bool resumeOnMainThread = this.IsJoinableTaskAware(out _, out JoinableTaskCollection? joinableTaskCollection) - ? joinableTaskCollection.Context.IsOnMainThread - : false; - bool mustYield = false; - using (this.joinableTaskCollection?.Join()) + if (this.IsJoinableTaskAware(out JoinableTaskFactory? joinableTaskFactory, out _)) { - if (this.IsJoinableTaskAware(out _, out _)) + if (resumeOnMainThread) { - // Use ConfiguredAwaitRunInline() as ConfigureAwait(true) will - // deadlock due to not being inside a JTF.RunAsync(). - Task? releaserTask = this.semaphore.EnterAsync(cancellationToken); - - // Yield to prevent running on the stack that released the semaphore. - mustYield = !releaserTask.IsCompleted; - releaser = await releaserTask.ConfigureAwaitRunInline(); + // Return to the main thread if we started there. + await joinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: mustYield, cancellationToken); } else { - releaser = await this.semaphore.EnterAsync(cancellationToken).ConfigureAwait(true); + await TaskScheduler.Default.SwitchTo(alwaysYield: mustYield); } } - return await this.ExecuteCoreAsync(async delegate + // The semaphore faulted while we were waiting on it. + this.ThrowIfFaulted(); + + lock (reentrantStack) + { + reentrantStack.Push(pushedReleaser); + pushed = true; + } + + return await operation().ConfigureAwait(true); + }); + } + finally + { + try + { + if (pushed) { - if (this.IsJoinableTaskAware(out JoinableTaskFactory? joinableTaskFactory, out _)) + lock (reentrantStack) { - if (resumeOnMainThread) - { - // Return to the main thread if we started there. - await joinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: mustYield, cancellationToken); - } - else + StrongBox? poppedReleaser = reentrantStack.Pop(); + if (!object.ReferenceEquals(poppedReleaser, pushedReleaser)) { - await TaskScheduler.Default.SwitchTo(alwaysYield: mustYield); + // When the semaphore faults, we will drain and throw for awaiting tasks one by one. + this.faulted = true; +#pragma warning disable CA2219 // Do not raise exceptions in finally clauses + throw new IllegalSemaphoreUsageException(string.Format(CultureInfo.CurrentCulture, Strings.SemaphoreStackNestingViolated, ReentrantSemaphore.ReentrancyMode.Stack)); +#pragma warning restore CA2219 // Do not raise exceptions in finally clauses } } - - this.reentrancyDetection.Value = ownedBox = new StrongBox(true); - return await operation().ConfigureAwait(true); - }); + } } finally { - // Make it clear to any forks of our ExecutionContexxt that the semaphore is no longer owned. - // Null check incase the switch to UI thread was cancelled. - if (ownedBox is object) - { - ownedBox.Value = false; - } - DisposeReleaserNoThrow(releaser); } } + } - /// - public override RevertRelevance SuppressRelevance() - { - StrongBox? originalValue = this.reentrancyDetection.Value; - this.reentrancyDetection.Value = null; - return new RevertRelevance((t, s) => ((NotAllowedSemaphore)t).reentrancyDetection.Value = (StrongBox?)s, this, originalValue); - } + /// + public override RevertRelevance SuppressRelevance() + { + Stack>? originalValue = this.reentrantCount.Value; + this.reentrantCount.Value = null; + return new RevertRelevance((t, s) => ((StackSemaphore)t).reentrantCount.Value = (Stack>?)s, this, originalValue); } /// - /// An implementation of supporting the mode. + /// Throws an exception if this instance has been faulted. /// - private class StackSemaphore : ReentrantSemaphore + protected override void ThrowIfFaulted() { - /// - /// The means to recognize that a caller has already entered the semaphore. - /// - /// - /// We use instead of just here - /// so that we have a unique identity for each Releaser that we can recognize as a means to verify - /// the integrity of the "stack" of semaphore reentrant requests. - /// - private readonly AsyncLocal>> reentrantCount = new AsyncLocal>>(); - - /// - /// A flag to indicate this instance was misused and the data it protects should not be touched as it may be corrupted. - /// - private bool faulted; - - /// - /// Initializes a new instance of the class. - /// - /// The initial number of concurrent operations to allow. - /// The to use to mitigate deadlocks. - internal StackSemaphore(int initialCount, JoinableTaskContext? joinableTaskContext) - : base(initialCount, joinableTaskContext) + if (this.faulted) { + throw new SemaphoreFaultedException(); } + } + } - /// - public override async Task ExecuteAsync(Func operation, CancellationToken cancellationToken = default) - { - Requires.NotNull(operation, nameof(operation)); - this.ThrowIfFaulted(); - - // No race condition here: We're accessing AsyncLocal which we by definition have our own copy of. - // Multiple threads or multiple async methods will all have their own storage for this field. - Stack>? reentrantStack = this.reentrantCount.Value; - if (reentrantStack is null || reentrantStack.Count == 0) - { - // When the stack is empty, the semaphore isn't held. But many execution contexts that forked from a common root - // would be sharing this same empty Stack instance. If we pushed to that Stack, all those forks would suddenly - // be seen as having entered this new top-level semaphore. We therefore allocate a new Stack and assign it to our - // AsyncLocal field so that only this particular ExecutionContext is seen as having entered the semaphore. - this.reentrantCount.Value = reentrantStack = new Stack>(capacity: 2); - } + /// + /// An implementation of supporting the mode. + /// + private class FreeformSemaphore : ReentrantSemaphore + { + /// + /// The means to recognize that a caller has already entered the semaphore. + /// + private readonly AsyncLocal> reentrantCount = new AsyncLocal>(); - // Note: this code is duplicated and not extracted to minimize allocating extra async state machines. - // For performance reasons in the JTF enabled scenario, we want to minimize the number of Joins performed, and also - // keep the size of the JoinableCollection to a minimum. This also means awaiting on the semaphore outside of a - // JTF.RunAsync. This requires us to not ConfigureAwait(true) on the semaphore. However, that prevents us from - // resuming on the correct sync context. To partially fix this, we will at least resume you on the main thread or - // thread pool. - AsyncSemaphore.Releaser releaser = default; - bool pushed = false; - StrongBox? pushedReleaser = null; - try - { - bool resumeOnMainThread = this.IsJoinableTaskAware(out _, out JoinableTaskCollection? joinableTaskCollection) - ? joinableTaskCollection.Context.IsOnMainThread - : false; - bool mustYield = false; - if (reentrantStack.Count == 0) - { - using (this.joinableTaskCollection?.Join()) - { - if (this.IsJoinableTaskAware(out _, out _)) - { - // Use ConfiguredAwaitRunInline() as ConfigureAwait(true) will - // deadlock due to not being inside a JTF.RunAsync(). - Task? releaserTask = this.semaphore.EnterAsync(cancellationToken); + /// + /// Initializes a new instance of the class. + /// + /// The initial number of concurrent operations to allow. + /// The to use to mitigate deadlocks. + internal FreeformSemaphore(int initialCount, JoinableTaskContext? joinableTaskContext) + : base(initialCount, joinableTaskContext) + { + } - // Yield to prevent running on the stack that released the semaphore. - mustYield = !releaserTask.IsCompleted; + /// + public override async Task ExecuteAsync(Func operation, CancellationToken cancellationToken = default) + { + Requires.NotNull(operation, nameof(operation)); + this.ThrowIfFaulted(); - releaser = await releaserTask.ConfigureAwaitRunInline(); - } - else - { - releaser = await this.semaphore.EnterAsync(cancellationToken).ConfigureAwait(true); - } - } - } - else - { - releaser = default; - } + // No race condition here: We're accessing AsyncLocal which we by definition have our own copy of. + // Multiple threads or multiple async methods will all have their own storage for this field. + Stack? reentrantStack = this.reentrantCount.Value; + if (reentrantStack is null || reentrantStack.Count == 0) + { + this.reentrantCount.Value = reentrantStack = new Stack(capacity: 2); + } - pushedReleaser = new StrongBox(releaser); - await this.ExecuteCoreAsync(async delegate + // Note: this code is duplicated and not extracted to minimize allocating extra async state machines. + // For performance reasons in the JTF enabled scenario, we want to minimize the number of Joins performed, and also + // keep the size of the JoinableCollection to a minimum. This also means awaiting on the semaphore outside of a + // JTF.RunAsync. This requires us to not ConfigureAwait(true) on the semaphore. However, that prevents us from + // resuming on the correct sync context. To partially fix this, we will at least resume you on the main thread or + // thread pool. + AsyncSemaphore.Releaser releaser = default; + bool pushed = false; + try + { + bool resumeOnMainThread = this.IsJoinableTaskAware(out _, out JoinableTaskCollection? joinableTaskCollection) + ? joinableTaskCollection.Context.IsOnMainThread + : false; + bool mustYield = false; + if (reentrantStack.Count == 0) + { + using (this.joinableTaskCollection?.Join()) { - if (this.IsJoinableTaskAware(out JoinableTaskFactory? joinableTaskFactory, out _)) + if (this.IsJoinableTaskAware(out _, out _)) { - if (resumeOnMainThread) - { - // Return to the main thread if we started there. - await joinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: mustYield, cancellationToken); - } - else - { - await TaskScheduler.Default.SwitchTo(alwaysYield: mustYield); - } - } + // Use ConfiguredAwaitRunInline() as ConfigureAwait(true) will + // deadlock due to not being inside a JTF.RunAsync(). + Task? releaserTask = this.semaphore.EnterAsync(cancellationToken); - // The semaphore faulted while we were waiting on it. - this.ThrowIfFaulted(); + // Yield to prevent running on the stack that released the semaphore. + mustYield = !releaserTask.IsCompleted; - lock (reentrantStack) - { - reentrantStack.Push(pushedReleaser); - pushed = true; + releaser = await releaserTask.ConfigureAwaitRunInline(); } - - await operation().ConfigureAwaitRunInline(); - }); - } - finally - { - try - { - if (pushed) + else { - lock (reentrantStack) - { - StrongBox? poppedReleaser = reentrantStack.Pop(); - if (!object.ReferenceEquals(poppedReleaser, pushedReleaser)) - { - // When the semaphore faults, we will drain and throw for awaiting tasks one by one. - this.faulted = true; -#pragma warning disable CA2219 // Do not raise exceptions in finally clauses - throw new IllegalSemaphoreUsageException(string.Format(CultureInfo.CurrentCulture, Strings.SemaphoreStackNestingViolated, ReentrantSemaphore.ReentrancyMode.Stack)); -#pragma warning restore CA2219 // Do not raise exceptions in finally clauses - } - } + releaser = await this.semaphore.EnterAsync(cancellationToken).ConfigureAwait(true); } } - finally - { - DisposeReleaserNoThrow(releaser); - } } - } - - /// - public override async ValueTask ExecuteAsync(Func> operation, CancellationToken cancellationToken = default) - { - Requires.NotNull(operation, nameof(operation)); - this.ThrowIfFaulted(); - - // No race condition here: We're accessing AsyncLocal which we by definition have our own copy of. - // Multiple threads or multiple async methods will all have their own storage for this field. - Stack>? reentrantStack = this.reentrantCount.Value; - if (reentrantStack is null || reentrantStack.Count == 0) + else { - // When the stack is empty, the semaphore isn't held. But many execution contexts that forked from a common root - // would be sharing this same empty Stack instance. If we pushed to that Stack, all those forks would suddenly - // be seen as having entered this new top-level semaphore. We therefore allocate a new Stack and assign it to our - // AsyncLocal field so that only this particular ExecutionContext is seen as having entered the semaphore. - this.reentrantCount.Value = reentrantStack = new Stack>(capacity: 2); + releaser = default; } - // Note: this code is duplicated and not extracted to minimize allocating extra async state machines. - // For performance reasons in the JTF enabled scenario, we want to minimize the number of Joins performed, and also - // keep the size of the JoinableCollection to a minimum. This also means awaiting on the semaphore outside of a - // JTF.RunAsync. This requires us to not ConfigureAwait(true) on the semaphore. However, that prevents us from - // resuming on the correct sync context. To partially fix this, we will at least resume you on the main thread or - // thread pool. - AsyncSemaphore.Releaser releaser = default; - bool pushed = false; - StrongBox? pushedReleaser = null; - try + await this.ExecuteCoreAsync(async delegate { - bool resumeOnMainThread = this.IsJoinableTaskAware(out _, out JoinableTaskCollection? joinableTaskCollection) - ? joinableTaskCollection.Context.IsOnMainThread - : false; - bool mustYield = false; - if (reentrantStack.Count == 0) - { - using (this.joinableTaskCollection?.Join()) - { - if (this.IsJoinableTaskAware(out _, out _)) - { - // Use ConfiguredAwaitRunInline() as ConfigureAwait(true) will - // deadlock due to not being inside a JTF.RunAsync(). - Task? releaserTask = this.semaphore.EnterAsync(cancellationToken); - - // Yield to prevent running on the stack that released the semaphore. - mustYield = !releaserTask.IsCompleted; - - releaser = await releaserTask.ConfigureAwaitRunInline(); - } - else - { - releaser = await this.semaphore.EnterAsync(cancellationToken).ConfigureAwait(true); - } - } - } - else - { - releaser = default; - } - - pushedReleaser = new StrongBox(releaser); - return await this.ExecuteCoreAsync(async delegate + if (this.IsJoinableTaskAware(out JoinableTaskFactory? joinableTaskFactory, out _)) { - if (this.IsJoinableTaskAware(out JoinableTaskFactory? joinableTaskFactory, out _)) - { - if (resumeOnMainThread) - { - // Return to the main thread if we started there. - await joinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: mustYield, cancellationToken); - } - else - { - await TaskScheduler.Default.SwitchTo(alwaysYield: mustYield); - } - } - - // The semaphore faulted while we were waiting on it. - this.ThrowIfFaulted(); - - lock (reentrantStack) + if (resumeOnMainThread) { - reentrantStack.Push(pushedReleaser); - pushed = true; + // Return to the main thread if we started there. + await joinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: mustYield, cancellationToken); } - - return await operation().ConfigureAwait(true); - }); - } - finally - { - try - { - if (pushed) + else { - lock (reentrantStack) - { - StrongBox? poppedReleaser = reentrantStack.Pop(); - if (!object.ReferenceEquals(poppedReleaser, pushedReleaser)) - { - // When the semaphore faults, we will drain and throw for awaiting tasks one by one. - this.faulted = true; -#pragma warning disable CA2219 // Do not raise exceptions in finally clauses - throw new IllegalSemaphoreUsageException(string.Format(CultureInfo.CurrentCulture, Strings.SemaphoreStackNestingViolated, ReentrantSemaphore.ReentrancyMode.Stack)); -#pragma warning restore CA2219 // Do not raise exceptions in finally clauses - } - } + await TaskScheduler.Default.SwitchTo(alwaysYield: mustYield); } } - finally + + lock (reentrantStack) { - DisposeReleaserNoThrow(releaser); + reentrantStack.Push(releaser); + pushed = true; + releaser = default; // we should release whatever we pop off the stack (which ensures the last surviving nested holder actually releases). } - } - } - /// - public override RevertRelevance SuppressRelevance() - { - Stack>? originalValue = this.reentrantCount.Value; - this.reentrantCount.Value = null; - return new RevertRelevance((t, s) => ((StackSemaphore)t).reentrantCount.Value = (Stack>?)s, this, originalValue); + await operation().ConfigureAwaitRunInline(); + }); } - - /// - /// Throws an exception if this instance has been faulted. - /// - protected override void ThrowIfFaulted() + finally { - if (this.faulted) + if (pushed) { - throw new SemaphoreFaultedException(); + lock (reentrantStack) + { + releaser = reentrantStack.Pop(); + } } + + DisposeReleaserNoThrow(releaser); } } - /// - /// An implementation of supporting the mode. - /// - private class FreeformSemaphore : ReentrantSemaphore + /// + public override async ValueTask ExecuteAsync(Func> operation, CancellationToken cancellationToken = default) { - /// - /// The means to recognize that a caller has already entered the semaphore. - /// - private readonly AsyncLocal> reentrantCount = new AsyncLocal>(); - - /// - /// Initializes a new instance of the class. - /// - /// The initial number of concurrent operations to allow. - /// The to use to mitigate deadlocks. - internal FreeformSemaphore(int initialCount, JoinableTaskContext? joinableTaskContext) - : base(initialCount, joinableTaskContext) + Requires.NotNull(operation, nameof(operation)); + this.ThrowIfFaulted(); + + // No race condition here: We're accessing AsyncLocal which we by definition have our own copy of. + // Multiple threads or multiple async methods will all have their own storage for this field. + Stack? reentrantStack = this.reentrantCount.Value; + if (reentrantStack is null || reentrantStack.Count == 0) { + this.reentrantCount.Value = reentrantStack = new Stack(capacity: 2); } - /// - public override async Task ExecuteAsync(Func operation, CancellationToken cancellationToken = default) + // Note: this code is duplicated and not extracted to minimize allocating extra async state machines. + // For performance reasons in the JTF enabled scenario, we want to minimize the number of Joins performed, and also + // keep the size of the JoinableCollection to a minimum. This also means awaiting on the semaphore outside of a + // JTF.RunAsync. This requires us to not ConfigureAwait(true) on the semaphore. However, that prevents us from + // resuming on the correct sync context. To partially fix this, we will at least resume you on the main thread or + // thread pool. + AsyncSemaphore.Releaser releaser = default; + bool pushed = false; + try { - Requires.NotNull(operation, nameof(operation)); - this.ThrowIfFaulted(); - - // No race condition here: We're accessing AsyncLocal which we by definition have our own copy of. - // Multiple threads or multiple async methods will all have their own storage for this field. - Stack? reentrantStack = this.reentrantCount.Value; - if (reentrantStack is null || reentrantStack.Count == 0) - { - this.reentrantCount.Value = reentrantStack = new Stack(capacity: 2); - } - - // Note: this code is duplicated and not extracted to minimize allocating extra async state machines. - // For performance reasons in the JTF enabled scenario, we want to minimize the number of Joins performed, and also - // keep the size of the JoinableCollection to a minimum. This also means awaiting on the semaphore outside of a - // JTF.RunAsync. This requires us to not ConfigureAwait(true) on the semaphore. However, that prevents us from - // resuming on the correct sync context. To partially fix this, we will at least resume you on the main thread or - // thread pool. - AsyncSemaphore.Releaser releaser = default; - bool pushed = false; - try + bool resumeOnMainThread = this.IsJoinableTaskAware(out _, out JoinableTaskCollection? joinableTaskCollection) + ? joinableTaskCollection.Context.IsOnMainThread + : false; + bool mustYield = false; + if (reentrantStack.Count == 0) { - bool resumeOnMainThread = this.IsJoinableTaskAware(out _, out JoinableTaskCollection? joinableTaskCollection) - ? joinableTaskCollection.Context.IsOnMainThread - : false; - bool mustYield = false; - if (reentrantStack.Count == 0) + using (this.joinableTaskCollection?.Join()) { - using (this.joinableTaskCollection?.Join()) + if (this.IsJoinableTaskAware(out _, out _)) { - if (this.IsJoinableTaskAware(out _, out _)) - { - // Use ConfiguredAwaitRunInline() as ConfigureAwait(true) will - // deadlock due to not being inside a JTF.RunAsync(). - Task? releaserTask = this.semaphore.EnterAsync(cancellationToken); - - // Yield to prevent running on the stack that released the semaphore. - mustYield = !releaserTask.IsCompleted; - - releaser = await releaserTask.ConfigureAwaitRunInline(); - } - else - { - releaser = await this.semaphore.EnterAsync(cancellationToken).ConfigureAwait(true); - } - } - } - else - { - releaser = default; - } + // Use ConfiguredAwaitRunInline() as ConfigureAwait(true) will + // deadlock due to not being inside a JTF.RunAsync(). + Task? releaserTask = this.semaphore.EnterAsync(cancellationToken); - await this.ExecuteCoreAsync(async delegate - { - if (this.IsJoinableTaskAware(out JoinableTaskFactory? joinableTaskFactory, out _)) - { - if (resumeOnMainThread) - { - // Return to the main thread if we started there. - await joinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: mustYield, cancellationToken); - } - else - { - await TaskScheduler.Default.SwitchTo(alwaysYield: mustYield); - } - } + // Yield to prevent running on the stack that released the semaphore. + mustYield = !releaserTask.IsCompleted; - lock (reentrantStack) - { - reentrantStack.Push(releaser); - pushed = true; - releaser = default; // we should release whatever we pop off the stack (which ensures the last surviving nested holder actually releases). + releaser = await releaserTask.ConfigureAwaitRunInline(); } - - await operation().ConfigureAwaitRunInline(); - }); - } - finally - { - if (pushed) - { - lock (reentrantStack) + else { - releaser = reentrantStack.Pop(); + releaser = await this.semaphore.EnterAsync(cancellationToken).ConfigureAwait(true); } } - - DisposeReleaserNoThrow(releaser); } - } - - /// - public override async ValueTask ExecuteAsync(Func> operation, CancellationToken cancellationToken = default) - { - Requires.NotNull(operation, nameof(operation)); - this.ThrowIfFaulted(); - - // No race condition here: We're accessing AsyncLocal which we by definition have our own copy of. - // Multiple threads or multiple async methods will all have their own storage for this field. - Stack? reentrantStack = this.reentrantCount.Value; - if (reentrantStack is null || reentrantStack.Count == 0) + else { - this.reentrantCount.Value = reentrantStack = new Stack(capacity: 2); + releaser = default; } - // Note: this code is duplicated and not extracted to minimize allocating extra async state machines. - // For performance reasons in the JTF enabled scenario, we want to minimize the number of Joins performed, and also - // keep the size of the JoinableCollection to a minimum. This also means awaiting on the semaphore outside of a - // JTF.RunAsync. This requires us to not ConfigureAwait(true) on the semaphore. However, that prevents us from - // resuming on the correct sync context. To partially fix this, we will at least resume you on the main thread or - // thread pool. - AsyncSemaphore.Releaser releaser = default; - bool pushed = false; - try + return await this.ExecuteCoreAsync(async delegate { - bool resumeOnMainThread = this.IsJoinableTaskAware(out _, out JoinableTaskCollection? joinableTaskCollection) - ? joinableTaskCollection.Context.IsOnMainThread - : false; - bool mustYield = false; - if (reentrantStack.Count == 0) + if (this.IsJoinableTaskAware(out JoinableTaskFactory? joinableTaskFactory, out _)) { - using (this.joinableTaskCollection?.Join()) + if (resumeOnMainThread) { - if (this.IsJoinableTaskAware(out _, out _)) - { - // Use ConfiguredAwaitRunInline() as ConfigureAwait(true) will - // deadlock due to not being inside a JTF.RunAsync(). - Task? releaserTask = this.semaphore.EnterAsync(cancellationToken); - - // Yield to prevent running on the stack that released the semaphore. - mustYield = !releaserTask.IsCompleted; - - releaser = await releaserTask.ConfigureAwaitRunInline(); - } - else - { - releaser = await this.semaphore.EnterAsync(cancellationToken).ConfigureAwait(true); - } + // Return to the main thread if we started there. + await joinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: mustYield, cancellationToken); } - } - else - { - releaser = default; - } - - return await this.ExecuteCoreAsync(async delegate - { - if (this.IsJoinableTaskAware(out JoinableTaskFactory? joinableTaskFactory, out _)) + else { - if (resumeOnMainThread) - { - // Return to the main thread if we started there. - await joinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: mustYield, cancellationToken); - } - else - { - await TaskScheduler.Default.SwitchTo(alwaysYield: mustYield); - } + await TaskScheduler.Default.SwitchTo(alwaysYield: mustYield); } + } - lock (reentrantStack) - { - reentrantStack.Push(releaser); - pushed = true; - releaser = default; // we should release whatever we pop off the stack (which ensures the last surviving nested holder actually releases). - } + lock (reentrantStack) + { + reentrantStack.Push(releaser); + pushed = true; + releaser = default; // we should release whatever we pop off the stack (which ensures the last surviving nested holder actually releases). + } - return await operation().ConfigureAwait(true); - }); - } - finally + return await operation().ConfigureAwait(true); + }); + } + finally + { + if (pushed) { - if (pushed) + lock (reentrantStack) { - lock (reentrantStack) - { - releaser = reentrantStack.Pop(); - } + releaser = reentrantStack.Pop(); } - - DisposeReleaserNoThrow(releaser); } - } - /// - public override RevertRelevance SuppressRelevance() - { - Stack? originalValue = this.reentrantCount.Value; - this.reentrantCount.Value = null; - return new RevertRelevance((t, s) => ((FreeformSemaphore)t).reentrantCount.Value = (Stack?)s, this, originalValue); + DisposeReleaserNoThrow(releaser); } } + + /// + public override RevertRelevance SuppressRelevance() + { + Stack? originalValue = this.reentrantCount.Value; + this.reentrantCount.Value = null; + return new RevertRelevance((t, s) => ((FreeformSemaphore)t).reentrantCount.Value = (Stack?)s, this, originalValue); + } } } diff --git a/src/Microsoft.VisualStudio.Threading/RegistryChangeNotificationFilters.cs b/src/Microsoft.VisualStudio.Threading/RegistryChangeNotificationFilters.cs index 0a2006672..8a7db1ab8 100644 --- a/src/Microsoft.VisualStudio.Threading/RegistryChangeNotificationFilters.cs +++ b/src/Microsoft.VisualStudio.Threading/RegistryChangeNotificationFilters.cs @@ -1,44 +1,40 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading -{ - using System; +using System; +using global::Windows.Win32.System.Registry; + +namespace Microsoft.VisualStudio.Threading; +/// +/// The various types of data within a registry key that generate notifications +/// when changed. +/// +/// +/// This enum matches the Win32 REG_NOTIFY_CHANGE_* constants. +/// +[Flags] +public enum RegistryChangeNotificationFilters +{ /// - /// The various types of data within a registry key that generate notifications - /// when changed. + /// Notify the caller if a subkey is added or deleted. /// - /// - /// This enum matches the Win32 REG_NOTIFY_CHANGE_* constants. - /// - [Flags] - public enum RegistryChangeNotificationFilters - { - /// - /// Notify the caller if a subkey is added or deleted. - /// Corresponds to Win32 value REG_NOTIFY_CHANGE_NAME. - /// - Subkey = 0x1, + Subkey = (int)REG_NOTIFY_FILTER.REG_NOTIFY_CHANGE_NAME, - /// - /// Notify the caller of changes to the attributes of the key, - /// such as the security descriptor information. - /// Corresponds to Win32 value REG_NOTIFY_CHANGE_ATTRIBUTES. - /// - Attributes = 0x2, + /// + /// Notify the caller of changes to the attributes of the key, + /// such as the security descriptor information. + /// + Attributes = (int)REG_NOTIFY_FILTER.REG_NOTIFY_CHANGE_ATTRIBUTES, - /// - /// Notify the caller of changes to a value of the key. This can - /// include adding or deleting a value, or changing an existing value. - /// Corresponds to Win32 value REG_NOTIFY_CHANGE_LAST_SET. - /// - Value = 0x4, + /// + /// Notify the caller of changes to a value of the key. This can + /// include adding or deleting a value, or changing an existing value. + /// + Value = (int)REG_NOTIFY_FILTER.REG_NOTIFY_CHANGE_LAST_SET, - /// - /// Notify the caller of changes to the security descriptor of the key. - /// Corresponds to Win32 value REG_NOTIFY_CHANGE_SECURITY. - /// - Security = 0x8, - } + /// + /// Notify the caller of changes to the security descriptor of the key. + /// + Security = (int)REG_NOTIFY_FILTER.REG_NOTIFY_CHANGE_SECURITY, } diff --git a/src/Microsoft.VisualStudio.Threading/RoslynDebug.cs b/src/Microsoft.VisualStudio.Threading/RoslynDebug.cs index e035ecb15..6ade94373 100644 --- a/src/Microsoft.VisualStudio.Threading/RoslynDebug.cs +++ b/src/Microsoft.VisualStudio.Threading/RoslynDebug.cs @@ -1,22 +1,21 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace System.Diagnostics -{ - using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; + +namespace System.Diagnostics; - internal static class RoslynDebug - { - /// - [Conditional("DEBUG")] - internal static void Assert([DoesNotReturnIf(false)] bool b) +internal static class RoslynDebug +{ + /// + [Conditional("DEBUG")] + internal static void Assert([DoesNotReturnIf(false)] bool b) #pragma warning disable SA1405 // Debug.Assert should provide message text - => Debug.Assert(b); + => Debug.Assert(b); #pragma warning restore SA1405 // Debug.Assert should provide message text - /// - [Conditional("DEBUG")] - internal static void Assert([DoesNotReturnIf(false)] bool b, string message) - => Debug.Assert(b, message); - } + /// + [Conditional("DEBUG")] + internal static void Assert([DoesNotReturnIf(false)] bool b, string message) + => Debug.Assert(b, message); } diff --git a/src/Microsoft.VisualStudio.Threading/SemaphoreFaultedException.cs b/src/Microsoft.VisualStudio.Threading/SemaphoreFaultedException.cs index d9412344c..f5dbafea1 100644 --- a/src/Microsoft.VisualStudio.Threading/SemaphoreFaultedException.cs +++ b/src/Microsoft.VisualStudio.Threading/SemaphoreFaultedException.cs @@ -1,21 +1,20 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading -{ - using System; +using System; + +namespace Microsoft.VisualStudio.Threading; +/// +/// Exception thrown when a is in a faulted state. +/// +public class SemaphoreFaultedException : InvalidOperationException +{ /// - /// Exception thrown when a is in a faulted state. + /// Initializes a new instance of the class. /// - public class SemaphoreFaultedException : InvalidOperationException + public SemaphoreFaultedException() + : base(Strings.SemaphoreMisused) { - /// - /// Initializes a new instance of the class. - /// - public SemaphoreFaultedException() - : base(Strings.SemaphoreMisused) - { - } } } diff --git a/src/Microsoft.VisualStudio.Threading/SingleThreadedSynchronizationContext.cs b/src/Microsoft.VisualStudio.Threading/SingleThreadedSynchronizationContext.cs index 1c19d542e..c99693784 100644 --- a/src/Microsoft.VisualStudio.Threading/SingleThreadedSynchronizationContext.cs +++ b/src/Microsoft.VisualStudio.Threading/SingleThreadedSynchronizationContext.cs @@ -1,245 +1,244 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// A single-threaded synchronization context, akin to the DispatcherSynchronizationContext +/// and WindowsFormsSynchronizationContext. +/// +/// +/// This must be created on the thread that will serve as the pumping thread. +/// +public class SingleThreadedSynchronizationContext : SynchronizationContext { - using System; - using System.Collections.Generic; - using System.Reflection; - using System.Threading; + /// + /// The list of posted messages to be executed. Must be locked for all access. + /// + private readonly Queue messageQueue; /// - /// A single-threaded synchronization context, akin to the DispatcherSynchronizationContext - /// and WindowsFormsSynchronizationContext. + /// The managed thread ID of the thread this instance owns. /// - /// - /// This must be created on the thread that will serve as the pumping thread. - /// - public class SingleThreadedSynchronizationContext : SynchronizationContext + private readonly int ownedThreadId; + + /// + /// Initializes a new instance of the class, + /// with the new instance affinitized to the current thread. + /// + public SingleThreadedSynchronizationContext() { - /// - /// The list of posted messages to be executed. Must be locked for all access. - /// - private readonly Queue messageQueue; + this.messageQueue = new Queue(); + this.ownedThreadId = Environment.CurrentManagedThreadId; + } - /// - /// The managed thread ID of the thread this instance owns. - /// - private readonly int ownedThreadId; + /// + /// Initializes a new instance of the class, + /// as an equivalent copy to another instance. + /// + private SingleThreadedSynchronizationContext(SingleThreadedSynchronizationContext copyFrom) + { + Requires.NotNull(copyFrom, nameof(copyFrom)); - /// - /// Initializes a new instance of the class, - /// with the new instance affinitized to the current thread. - /// - public SingleThreadedSynchronizationContext() + this.messageQueue = copyFrom.messageQueue; + this.ownedThreadId = copyFrom.ownedThreadId; + } + + /// + public override void Post(SendOrPostCallback d, object? state) + { + var ctxt = ExecutionContext.Capture(); + lock (this.messageQueue) { - this.messageQueue = new Queue(); - this.ownedThreadId = Environment.CurrentManagedThreadId; + this.messageQueue.Enqueue(new Message(d, state, ctxt)); + Monitor.PulseAll(this.messageQueue); } + } - /// - /// Initializes a new instance of the class, - /// as an equivalent copy to another instance. - /// - private SingleThreadedSynchronizationContext(SingleThreadedSynchronizationContext copyFrom) - { - Requires.NotNull(copyFrom, nameof(copyFrom)); + /// + public override void Send(SendOrPostCallback d, object? state) + { + Requires.NotNull(d, nameof(d)); - this.messageQueue = copyFrom.messageQueue; - this.ownedThreadId = copyFrom.ownedThreadId; + if (this.ownedThreadId == Environment.CurrentManagedThreadId) + { + try + { + d(state); + } + catch (Exception ex) + { + throw new TargetInvocationException(ex); + } } - - /// - public override void Post(SendOrPostCallback d, object? state) + else { + Exception? caughtException = null; + var evt = new ManualResetEventSlim(); var ctxt = ExecutionContext.Capture(); lock (this.messageQueue) { - this.messageQueue.Enqueue(new Message(d, state, ctxt)); + this.messageQueue.Enqueue(new Message( + s => + { + try + { + d(state); + } + catch (Exception ex) + { + caughtException = ex; + } + finally + { + evt.Set(); + } + }, + null, + ctxt)); Monitor.PulseAll(this.messageQueue); } - } - /// - public override void Send(SendOrPostCallback d, object? state) - { - Requires.NotNull(d, nameof(d)); - - if (this.ownedThreadId == Environment.CurrentManagedThreadId) - { - try - { - d(state); - } - catch (Exception ex) - { - throw new TargetInvocationException(ex); - } - } - else + evt.Wait(); + if (caughtException is object) { - Exception? caughtException = null; - var evt = new ManualResetEventSlim(); - var ctxt = ExecutionContext.Capture(); - lock (this.messageQueue) - { - this.messageQueue.Enqueue(new Message( - s => - { - try - { - d(state); - } - catch (Exception ex) - { - caughtException = ex; - } - finally - { - evt.Set(); - } - }, - null, - ctxt)); - Monitor.PulseAll(this.messageQueue); - } - - evt.Wait(); - if (caughtException is object) - { - throw new TargetInvocationException(caughtException); - } + throw new TargetInvocationException(caughtException); } } + } - /// - public override SynchronizationContext CreateCopy() - { - // Don't return "this", since that can result in the same instance being "Current" - // on another thread, and end up being misinterpreted as permission to skip the SyncContext - // and simply inline certain continuations by buggy code. - // See https://referencesource.microsoft.com/#WindowsBase/Base/System/Windows/BaseCompatibilityPreferences.cs,39 - return new SingleThreadedSynchronizationContext(this); - } + /// + public override SynchronizationContext CreateCopy() + { + // Don't return "this", since that can result in the same instance being "Current" + // on another thread, and end up being misinterpreted as permission to skip the SyncContext + // and simply inline certain continuations by buggy code. + // See https://referencesource.microsoft.com/#WindowsBase/Base/System/Windows/BaseCompatibilityPreferences.cs,39 + return new SingleThreadedSynchronizationContext(this); + } - /// - /// Pushes a message pump on the current thread that will execute work scheduled using . - /// - /// The frame to represent this message pump, which controls when the message pump ends. - public void PushFrame(Frame frame) - { - Requires.NotNull(frame, nameof(frame)); - Verify.Operation(this.ownedThreadId == Environment.CurrentManagedThreadId, Strings.PushFromWrongThread); - frame.SetOwner(this); + /// + /// Pushes a message pump on the current thread that will execute work scheduled using . + /// + /// The frame to represent this message pump, which controls when the message pump ends. + public void PushFrame(Frame frame) + { + Requires.NotNull(frame, nameof(frame)); + Verify.Operation(this.ownedThreadId == Environment.CurrentManagedThreadId, Strings.PushFromWrongThread); + frame.SetOwner(this); - using (this.Apply()) + using (this.Apply()) + { + while (frame.Continue) { - while (frame.Continue) + Message message; + lock (this.messageQueue) { - Message message; - lock (this.messageQueue) + // Check again now that we're holding the lock. + if (!frame.Continue) { - // Check again now that we're holding the lock. - if (!frame.Continue) - { - break; - } - - if (this.messageQueue.Count > 0) - { - message = this.messageQueue.Dequeue(); - } - else - { - Monitor.Wait(this.messageQueue); - continue; - } + break; } - if (message.Context is object) + if (this.messageQueue.Count > 0) { - ExecutionContext.Run( - message.Context, - new ContextCallback(message.Callback), - message.State); + message = this.messageQueue.Dequeue(); } else { - // If this throws, we intentionally let it propagate to our caller. - // WPF/WinForms SyncContexts will crash the process (perhaps by throwing from their method like this?). - // But anyway, throwing from here instead of crashing is more friendly IMO and more easily tested. - message.Callback(message.State); + Monitor.Wait(this.messageQueue); + continue; } } + + if (message.Context is object) + { + ExecutionContext.Run( + message.Context, + new ContextCallback(message.Callback), + message.State); + } + else + { + // If this throws, we intentionally let it propagate to our caller. + // WPF/WinForms SyncContexts will crash the process (perhaps by throwing from their method like this?). + // But anyway, throwing from here instead of crashing is more friendly IMO and more easily tested. + message.Callback(message.State); + } } } + } - private readonly struct Message - { - internal readonly SendOrPostCallback Callback; - internal readonly object? State; - internal readonly ExecutionContext? Context; + private readonly struct Message + { + internal readonly SendOrPostCallback Callback; + internal readonly object? State; + internal readonly ExecutionContext? Context; - internal Message(SendOrPostCallback d, object? state, ExecutionContext? ctxt) - { - this.Callback = d; - this.State = state; - this.Context = ctxt; - } + internal Message(SendOrPostCallback d, object? state, ExecutionContext? ctxt) + { + this.Callback = d; + this.State = state; + this.Context = ctxt; } + } + + /// + /// A message pumping frame that may be pushed with to pump messages + /// on the owning thread. + /// + public class Frame + { + /// + /// The owning sync context. + /// + private SingleThreadedSynchronizationContext? owner; + + /// + /// Backing field for the property. + /// + private bool @continue = true; /// - /// A message pumping frame that may be pushed with to pump messages - /// on the owning thread. + /// Gets or sets a value indicating whether a call to with this + /// should continue pumping messages or should return to its caller. /// - public class Frame + public bool Continue { - /// - /// The owning sync context. - /// - private SingleThreadedSynchronizationContext? owner; - - /// - /// Backing field for the property. - /// - private bool @continue = true; - - /// - /// Gets or sets a value indicating whether a call to with this - /// should continue pumping messages or should return to its caller. - /// - public bool Continue + get { - get - { - return this.@continue; - } + return this.@continue; + } - set - { - Verify.Operation(this.owner is object, Strings.FrameMustBePushedFirst); + set + { + Verify.Operation(this.owner is object, Strings.FrameMustBePushedFirst); - this.@continue = value; + this.@continue = value; - // Alert thread that may be blocked waiting for an incoming message - // that it no longer needs to wait. - if (!value) + // Alert thread that may be blocked waiting for an incoming message + // that it no longer needs to wait. + if (!value) + { + lock (this.owner.messageQueue) { - lock (this.owner.messageQueue) - { - Monitor.PulseAll(this.owner.messageQueue); - } + Monitor.PulseAll(this.owner.messageQueue); } } } + } - internal void SetOwner(SingleThreadedSynchronizationContext context) + internal void SetOwner(SingleThreadedSynchronizationContext context) + { + if (context != this.owner) { - if (context != this.owner) - { - Verify.Operation(this.owner is null, Strings.SyncContextFrameMismatchedAffinity); - this.owner = context; - } + Verify.Operation(this.owner is null, Strings.SyncContextFrameMismatchedAffinity); + this.owner = context; } } } diff --git a/src/Microsoft.VisualStudio.Threading/SpecializedSyncContext.cs b/src/Microsoft.VisualStudio.Threading/SpecializedSyncContext.cs index 6e5526d58..f6cf97bde 100644 --- a/src/Microsoft.VisualStudio.Threading/SpecializedSyncContext.cs +++ b/src/Microsoft.VisualStudio.Threading/SpecializedSyncContext.cs @@ -1,68 +1,67 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading -{ - using System; - using System.Threading; +using System; +using System.Threading; + +namespace Microsoft.VisualStudio.Threading; +/// +/// A structure that applies and reverts changes to the . +/// +public readonly struct SpecializedSyncContext : IDisposable +{ /// - /// A structure that applies and reverts changes to the . + /// A flag indicating whether the non-default constructor was invoked. /// - public readonly struct SpecializedSyncContext : IDisposable - { - /// - /// A flag indicating whether the non-default constructor was invoked. - /// - private readonly bool initialized; + private readonly bool initialized; - /// - /// The SynchronizationContext to restore when is invoked. - /// - private readonly SynchronizationContext? prior; + /// + /// The SynchronizationContext to restore when is invoked. + /// + private readonly SynchronizationContext? prior; - /// - /// The SynchronizationContext applied when this struct was constructed. - /// - private readonly SynchronizationContext? appliedContext; + /// + /// The SynchronizationContext applied when this struct was constructed. + /// + private readonly SynchronizationContext? appliedContext; - /// - /// A value indicating whether to check that the applied SyncContext is still the current one when the original is restored. - /// - private readonly bool checkForChangesOnRevert; + /// + /// A value indicating whether to check that the applied SyncContext is still the current one when the original is restored. + /// + private readonly bool checkForChangesOnRevert; - /// - /// Initializes a new instance of the struct. - /// - private SpecializedSyncContext(SynchronizationContext? syncContext, bool checkForChangesOnRevert) - { - this.initialized = true; - this.prior = SynchronizationContext.Current; - this.appliedContext = syncContext; - this.checkForChangesOnRevert = checkForChangesOnRevert; - SynchronizationContext.SetSynchronizationContext(syncContext); - } + /// + /// Initializes a new instance of the struct. + /// + private SpecializedSyncContext(SynchronizationContext? syncContext, bool checkForChangesOnRevert) + { + this.initialized = true; + this.prior = SynchronizationContext.Current; + this.appliedContext = syncContext; + this.checkForChangesOnRevert = checkForChangesOnRevert; + SynchronizationContext.SetSynchronizationContext(syncContext); + } - /// - /// Applies the specified to the caller's context. - /// - /// The synchronization context to apply. - /// A value indicating whether to check that the applied SyncContext is still the current one when the original is restored. - public static SpecializedSyncContext Apply(SynchronizationContext? syncContext, bool checkForChangesOnRevert = true) - { - return new SpecializedSyncContext(syncContext, checkForChangesOnRevert); - } + /// + /// Applies the specified to the caller's context. + /// + /// The synchronization context to apply. + /// A value indicating whether to check that the applied SyncContext is still the current one when the original is restored. + public static SpecializedSyncContext Apply(SynchronizationContext? syncContext, bool checkForChangesOnRevert = true) + { + return new SpecializedSyncContext(syncContext, checkForChangesOnRevert); + } - /// - /// Reverts the SynchronizationContext to its previous instance. - /// - public void Dispose() + /// + /// Reverts the SynchronizationContext to its previous instance. + /// + public void Dispose() + { + if (this.initialized) { - if (this.initialized) - { - Report.If(this.checkForChangesOnRevert && SynchronizationContext.Current != this.appliedContext); - SynchronizationContext.SetSynchronizationContext(this.prior); - } + Report.If(this.checkForChangesOnRevert && SynchronizationContext.Current != this.appliedContext); + SynchronizationContext.SetSynchronizationContext(this.prior); } } } diff --git a/src/Microsoft.VisualStudio.Threading/Strings.Designer.cs b/src/Microsoft.VisualStudio.Threading/Strings.Designer.cs deleted file mode 100644 index 195cf0993..000000000 --- a/src/Microsoft.VisualStudio.Threading/Strings.Designer.cs +++ /dev/null @@ -1,288 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Microsoft.VisualStudio.Threading { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Strings { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Strings() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.Threading.Strings", typeof(Strings).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Acquiring locks on threads with a SynchronizationContext applied is not allowed.. - /// - internal static string AppliedSynchronizationContextNotAllowed { - get { - return ResourceManager.GetString("AppliedSynchronizationContextNotAllowed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A non-upgradeable read lock is held by the caller and cannot be upgraded.. - /// - internal static string CannotUpgradeNonUpgradeableLock { - get { - return ResourceManager.GetString("CannotUpgradeNonUpgradeableLock", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Dangerous request for read lock from fork of write lock.. - /// - internal static string DangerousReadLockRequestFromWriteLockFork { - get { - return ResourceManager.GetString("DangerousReadLockRequestFromWriteLockFork", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This instance must be pushed first.. - /// - internal static string FrameMustBePushedFirst { - get { - return ResourceManager.GetString("FrameMustBePushedFirst", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Already transitioned to the Completed state.. - /// - internal static string InvalidAfterCompleted { - get { - return ResourceManager.GetString("InvalidAfterCompleted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This operation can only be executed against a valid lock.. - /// - internal static string InvalidLock { - get { - return ResourceManager.GetString("InvalidLock", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A lock is required.. - /// - internal static string InvalidWithoutLock { - get { - return ResourceManager.GetString("InvalidWithoutLock", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to JoinableTask does not belong to the context this collection was instantiated with.. - /// - internal static string JoinableTaskContextAndCollectionMismatch { - get { - return ResourceManager.GetString("JoinableTaskContextAndCollectionMismatch", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This node already registered.. - /// - internal static string JoinableTaskContextNodeAlreadyRegistered { - get { - return ResourceManager.GetString("JoinableTaskContextNodeAlreadyRegistered", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Lazily created value faulted during construction.. - /// - internal static string LazyValueFaulted { - get { - return ResourceManager.GetString("LazyValueFaulted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Lazily created value not yet constructed.. - /// - internal static string LazyValueNotCreated { - get { - return ResourceManager.GetString("LazyValueNotCreated", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This lock has already been marked for completion. No new top-level locks can be serviced.. - /// - internal static string LockCompletionAlreadyRequested { - get { - return ResourceManager.GetString("LockCompletionAlreadyRequested", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Multiple continuations are not supported.. - /// - internal static string MultipleContinuationsNotSupported { - get { - return ResourceManager.GetString("MultipleContinuationsNotSupported", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This operation is not allowed while holding an active upgradeable read or write lock from an AsyncReaderWriterLock.. - /// - internal static string NotAllowedUnderURorWLock { - get { - return ResourceManager.GetString("NotAllowedUnderURorWLock", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Message pump can only be run from the original thread.. - /// - internal static string PushFromWrongThread { - get { - return ResourceManager.GetString("PushFromWrongThread", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The queue is empty.. - /// - internal static string QueueEmpty { - get { - return ResourceManager.GetString("QueueEmpty", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Semaphore is already held and reentrancy setting is '{0}'.. - /// - internal static string SemaphoreAlreadyHeld { - get { - return ResourceManager.GetString("SemaphoreAlreadyHeld", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This semaphore has been misused and can no longer be used.. - /// - internal static string SemaphoreMisused { - get { - return ResourceManager.GetString("SemaphoreMisused", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Nested semaphore requests must be released in LIFO order when the reentrancy setting is: '{0}'. - /// - internal static string SemaphoreStackNestingViolated { - get { - return ResourceManager.GetString("SemaphoreStackNestingViolated", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This operation cannot be completed on an STA thread.. - /// - internal static string STAThreadCallerNotAllowed { - get { - return ResourceManager.GetString("STAThreadCallerNotAllowed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An attempt to switch to the main thread failed to reach the expected thread. Was the JoinableTaskContext initialized on the wrong thread or with a SynchronizationContext whose Post method does not execute its delegate on the main thread?. - /// - internal static string SwitchToMainThreadFailedToReachExpectedThread { - get { - return ResourceManager.GetString("SwitchToMainThreadFailedToReachExpectedThread", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This frame has already been used with a different instance.. - /// - internal static string SyncContextFrameMismatchedAffinity { - get { - return ResourceManager.GetString("SyncContextFrameMismatchedAffinity", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to No SynchronizationContext to reach the main thread has been set.. - /// - internal static string SyncContextNotSet { - get { - return ResourceManager.GetString("SyncContextNotSet", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The value factory has called for the value on the same instance.. - /// - internal static string ValueFactoryReentrancy { - get { - return ResourceManager.GetString("ValueFactoryReentrancy", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Write lock out-lived by a nested read lock, which is not allowed.. - /// - internal static string WriteLockOutlived { - get { - return ResourceManager.GetString("WriteLockOutlived", resourceCulture); - } - } - } -} diff --git a/src/Microsoft.VisualStudio.Threading/Strings.resx b/src/Microsoft.VisualStudio.Threading/Strings.resx index 2537d49ff..4983aee85 100644 --- a/src/Microsoft.VisualStudio.Threading/Strings.resx +++ b/src/Microsoft.VisualStudio.Threading/Strings.resx @@ -193,4 +193,7 @@ No SynchronizationContext to reach the main thread has been set. + + No JoinableTask is active. + \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.Threading/TaskCompletionSourceWithoutInlining`1.cs b/src/Microsoft.VisualStudio.Threading/TaskCompletionSourceWithoutInlining`1.cs deleted file mode 100644 index 2f233cdfa..000000000 --- a/src/Microsoft.VisualStudio.Threading/TaskCompletionSourceWithoutInlining`1.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Microsoft.VisualStudio.Threading -{ - using System; - using System.Diagnostics.CodeAnalysis; - using System.Threading; - using System.Threading.Tasks; - - /// - /// A -derivative that - /// does not inline continuations if so configured. - /// - /// The type of the task's resulting value. - internal class TaskCompletionSourceWithoutInlining : TaskCompletionSource - { - /// - /// The Task that we expose to others that may not inline continuations. - /// - private readonly Task exposedTask; - - /// - /// Initializes a new instance of the class. - /// - /// - /// true to allow continuations to be inlined; otherwise false. - /// - /// - /// TaskCreationOptions to pass on to the base constructor. - /// - /// The state to set on the Task. - internal TaskCompletionSourceWithoutInlining(bool allowInliningContinuations, TaskCreationOptions options = TaskCreationOptions.None, object? state = null) - : base(state, AdjustFlags(options, allowInliningContinuations)) - { - this.exposedTask = base.Task; - } - - /// - /// Gets the that may never complete inline with completion of this . - /// - /// - /// Return the base.Task if it is already completed since inlining continuations - /// on the completer is no longer a concern. Also, when we are not inlining continuations, - /// this.exposedTask completes slightly later than base.Task, and callers expect - /// the Task we return to be complete as soon as they call TrySetResult. - /// - internal new Task Task => base.Task.IsCompleted ? base.Task : this.exposedTask; - - /// - /// Modifies the specified flags to include RunContinuationsAsynchronously - /// if wanted by the caller and supported by the platform. - /// - /// The base options supplied by the caller. - /// true to allow inlining continuations. - /// The possibly modified flags. - private static TaskCreationOptions AdjustFlags(TaskCreationOptions options, bool allowInliningContinuations) - { - return allowInliningContinuations - ? (options & ~TaskCreationOptions.RunContinuationsAsynchronously) - : (options | TaskCreationOptions.RunContinuationsAsynchronously); - } - } -} diff --git a/src/Microsoft.VisualStudio.Threading/ThreadingEventSource.cs b/src/Microsoft.VisualStudio.Threading/ThreadingEventSource.cs index 4b98a0d40..59ae498a6 100644 --- a/src/Microsoft.VisualStudio.Threading/ThreadingEventSource.cs +++ b/src/Microsoft.VisualStudio.Threading/ThreadingEventSource.cs @@ -1,196 +1,197 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Tracing; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// The ETW source for logging events for this library. +/// +/// +/// We use a fully-descriptive type name because the type name becomes the name +/// of the ETW Provider. +/// +[EventSource(Name = "Microsoft-VisualStudio-Threading")] +internal sealed partial class ThreadingEventSource : EventSource { - using System; - using System.Diagnostics.Tracing; + /// + /// The singleton instance used for logging. + /// + internal static readonly ThreadingEventSource Instance = new ThreadingEventSource(); + + /// + /// The event ID for the event. + /// + private const int ReaderWriterLockIssuedLockCountsEvent = 1; + + /// + /// The event ID for the event. + /// + private const int WaitReaderWriterLockStartEvent = 2; + + /// + /// The event ID for the event. + /// + private const int WaitReaderWriterLockStopEvent = 3; + + /// + /// The event ID for the . + /// + private const int CompleteOnCurrentThreadStartEvent = 11; + + /// + /// The event ID for the . + /// + private const int CompleteOnCurrentThreadStopEvent = 12; + + /// + /// The event ID for the . + /// + private const int WaitSynchronouslyStartEvent = 13; + + /// + /// The event ID for the . + /// + private const int WaitSynchronouslyStopEvent = 14; + + /// + /// The event ID for the . + /// + private const int PostExecutionStartEvent = 15; + + /// + /// The event ID for the . + /// + private const int PostExecutionStopEvent = 16; + + /// + /// The event ID for the . + /// + private const int CircularJoinableTaskDependencyDetectedEvent = 17; + + /// + /// Logs an issued lock. + /// + [Event(ReaderWriterLockIssuedLockCountsEvent, Task = Tasks.LockRequest, Opcode = Opcodes.ReaderWriterLockIssued)] + public void ReaderWriterLockIssued(int lockId, AsyncReaderWriterLock.LockKind kind, int issuedUpgradeableReadCount, int issuedReadCount) + { + this.WriteEvent(ReaderWriterLockIssuedLockCountsEvent, lockId, kind, issuedUpgradeableReadCount, issuedReadCount); + } + + /// + /// Logs a wait for a lock. + /// + [Event(WaitReaderWriterLockStartEvent, Task = Tasks.LockRequestContention, Opcode = EventOpcode.Start)] + public void WaitReaderWriterLockStart(int lockId, AsyncReaderWriterLock.LockKind kind, int issuedWriteCount, int issuedUpgradeableReadCount, int issuedReadCount) + { + this.WriteEvent(WaitReaderWriterLockStartEvent, lockId, kind, issuedWriteCount, issuedUpgradeableReadCount, issuedReadCount); + } + + /// + /// Logs a lock that was issued after a contending lock was released. + /// + [Event(WaitReaderWriterLockStopEvent, Task = Tasks.LockRequestContention, Opcode = EventOpcode.Stop)] + public void WaitReaderWriterLockStop(int lockId, AsyncReaderWriterLock.LockKind kind) + { + this.WriteEvent(WaitReaderWriterLockStopEvent, lockId, kind); + } + + /// + /// Enters a synchronously task. + /// + /// Hash code of the task. + /// Whether the task is on the main thread. + [Event(CompleteOnCurrentThreadStartEvent)] + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "We're only serializing primitive types.")] + public void CompleteOnCurrentThreadStart(int taskId, bool isOnMainThread) + { + this.WriteEvent(CompleteOnCurrentThreadStartEvent, taskId, Boxed.Box(isOnMainThread)); + } + + /// + /// Exits a synchronously task. + /// + /// Hash code of the task. + [Event(CompleteOnCurrentThreadStopEvent)] + public void CompleteOnCurrentThreadStop(int taskId) + { + this.WriteEvent(CompleteOnCurrentThreadStopEvent, taskId); + } + + /// + /// The current thread starts to wait on execution requests. + /// + [Event(WaitSynchronouslyStartEvent, Level = EventLevel.Verbose)] + public void WaitSynchronouslyStart() + { + this.WriteEvent(WaitSynchronouslyStartEvent); + } + + /// + /// The current thread gets an execution request. + /// + [Event(WaitSynchronouslyStopEvent, Level = EventLevel.Verbose)] + public void WaitSynchronouslyStop() + { + this.WriteEvent(WaitSynchronouslyStopEvent); + } + + /// + /// Post a execution request to the queue. + /// + /// The request id. + /// The execution need happen on the main thread. + [Event(PostExecutionStartEvent, Level = EventLevel.Verbose)] + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "We're only serializing primitive types.")] + public void PostExecutionStart(int requestId, bool mainThreadAffinitized) + { + this.WriteEvent(PostExecutionStartEvent, requestId, Boxed.Box(mainThreadAffinitized)); + } + + /// + /// An execution request is processed. + /// + /// The request id. + [Event(PostExecutionStopEvent, Level = EventLevel.Verbose)] + public void PostExecutionStop(int requestId) + { + this.WriteEvent(PostExecutionStopEvent, requestId); + } + + /// + /// Circular JoinableTask dependency detected. + /// + /// Initial count of unreachable nodes. + /// The size of the connected dependency graph. + [Event(CircularJoinableTaskDependencyDetectedEvent, Level = EventLevel.Informational)] + public void CircularJoinableTaskDependencyDetected(int initUnreachableCount, int reachableCount) + { + this.WriteEvent(CircularJoinableTaskDependencyDetectedEvent, initUnreachableCount, reachableCount); + } + + /// + /// The names of constants in this class make up the middle term in + /// the AsyncReaderWriterLock/LockRequest/Issued event name. + /// + /// The name of this class is important for EventSource. + public static class Tasks + { + public const EventTask LockRequest = (EventTask)1; + public const EventTask LockRequestContention = (EventTask)2; + } /// - /// The ETW source for logging events for this library. + /// The names of constants in this class make up the last term in + /// the AsyncReaderWriterLock/LockRequest/Issued event name. /// - /// - /// We use a fully-descriptive type name because the type name becomes the name - /// of the ETW Provider. - /// - [EventSource(Name = "Microsoft-VisualStudio-Threading")] - internal sealed partial class ThreadingEventSource : EventSource + /// The name of this class is important for EventSource. + public static class Opcodes { - /// - /// The singleton instance used for logging. - /// - internal static readonly ThreadingEventSource Instance = new ThreadingEventSource(); - - /// - /// The event ID for the event. - /// - private const int ReaderWriterLockIssuedLockCountsEvent = 1; - - /// - /// The event ID for the event. - /// - private const int WaitReaderWriterLockStartEvent = 2; - - /// - /// The event ID for the event. - /// - private const int WaitReaderWriterLockStopEvent = 3; - - /// - /// The event ID for the . - /// - private const int CompleteOnCurrentThreadStartEvent = 11; - - /// - /// The event ID for the . - /// - private const int CompleteOnCurrentThreadStopEvent = 12; - - /// - /// The event ID for the . - /// - private const int WaitSynchronouslyStartEvent = 13; - - /// - /// The event ID for the . - /// - private const int WaitSynchronouslyStopEvent = 14; - - /// - /// The event ID for the . - /// - private const int PostExecutionStartEvent = 15; - - /// - /// The event ID for the . - /// - private const int PostExecutionStopEvent = 16; - - /// - /// The event ID for the . - /// - private const int CircularJoinableTaskDependencyDetectedEvent = 17; - - /// - /// Logs an issued lock. - /// - [Event(ReaderWriterLockIssuedLockCountsEvent, Task = Tasks.LockRequest, Opcode = Opcodes.ReaderWriterLockIssued)] - public void ReaderWriterLockIssued(int lockId, AsyncReaderWriterLock.LockKind kind, int issuedUpgradeableReadCount, int issuedReadCount) - { - this.WriteEvent(ReaderWriterLockIssuedLockCountsEvent, lockId, kind, issuedUpgradeableReadCount, issuedReadCount); - } - - /// - /// Logs a wait for a lock. - /// - [Event(WaitReaderWriterLockStartEvent, Task = Tasks.LockRequestContention, Opcode = EventOpcode.Start)] - public void WaitReaderWriterLockStart(int lockId, AsyncReaderWriterLock.LockKind kind, int issuedWriteCount, int issuedUpgradeableReadCount, int issuedReadCount) - { - this.WriteEvent(WaitReaderWriterLockStartEvent, lockId, kind, issuedWriteCount, issuedUpgradeableReadCount, issuedReadCount); - } - - /// - /// Logs a lock that was issued after a contending lock was released. - /// - [Event(WaitReaderWriterLockStopEvent, Task = Tasks.LockRequestContention, Opcode = EventOpcode.Stop)] - public void WaitReaderWriterLockStop(int lockId, AsyncReaderWriterLock.LockKind kind) - { - this.WriteEvent(WaitReaderWriterLockStopEvent, lockId, kind); - } - - /// - /// Enters a synchronously task. - /// - /// Hash code of the task. - /// Whether the task is on the main thread. - [Event(CompleteOnCurrentThreadStartEvent)] - public void CompleteOnCurrentThreadStart(int taskId, bool isOnMainThread) - { - this.WriteEvent(CompleteOnCurrentThreadStartEvent, taskId, isOnMainThread); - } - - /// - /// Exits a synchronously task. - /// - /// Hash code of the task. - [Event(CompleteOnCurrentThreadStopEvent)] - public void CompleteOnCurrentThreadStop(int taskId) - { - this.WriteEvent(CompleteOnCurrentThreadStopEvent, taskId); - } - - /// - /// The current thread starts to wait on execution requests. - /// - [Event(WaitSynchronouslyStartEvent, Level = EventLevel.Verbose)] - public void WaitSynchronouslyStart() - { - this.WriteEvent(WaitSynchronouslyStartEvent); - } - - /// - /// The current thread gets an execution request. - /// - [Event(WaitSynchronouslyStopEvent, Level = EventLevel.Verbose)] - public void WaitSynchronouslyStop() - { - this.WriteEvent(WaitSynchronouslyStopEvent); - } - - /// - /// Post a execution request to the queue. - /// - /// The request id. - /// The execution need happen on the main thread. - [Event(PostExecutionStartEvent, Level = EventLevel.Verbose)] - public void PostExecutionStart(int requestId, bool mainThreadAffinitized) - { - this.WriteEvent(PostExecutionStartEvent, requestId, mainThreadAffinitized); - } - - /// - /// An execution request is processed. - /// - /// The request id. - [Event(PostExecutionStopEvent, Level = EventLevel.Verbose)] - public void PostExecutionStop(int requestId) - { - this.WriteEvent(PostExecutionStopEvent, requestId); - } - - /// - /// Circular JoinableTask dependency detected. - /// - /// Initial count of unreachable nodes. - /// The size of the connected dependency graph. - [Event(CircularJoinableTaskDependencyDetectedEvent, Level = EventLevel.Informational)] - public void CircularJoinableTaskDependencyDetected(int initUnreachableCount, int reachableCount) - { - this.WriteEvent(CircularJoinableTaskDependencyDetectedEvent, initUnreachableCount, reachableCount); - } - - /// - /// The names of constants in this class make up the middle term in - /// the AsyncReaderWriterLock/LockRequest/Issued event name. - /// - /// The name of this class is important for EventSource. - public static class Tasks - { - public const EventTask LockRequest = (EventTask)1; - public const EventTask LockRequestContention = (EventTask)2; - } - - /// - /// The names of constants in this class make up the last term in - /// the AsyncReaderWriterLock/LockRequest/Issued event name. - /// - /// The name of this class is important for EventSource. - public static class Opcodes - { - // Custom opcodes should range 11 - 239; see http://msdn.microsoft.com/en-us/library/windows/desktop/dd996918(v=vs.85).aspx - public const EventOpcode ReaderWriterLockWaiting = (EventOpcode)100; - public const EventOpcode ReaderWriterLockIssued = (EventOpcode)101; - public const EventOpcode ReaderWriterLockIssuedAfterContention = (EventOpcode)102; - } + // Custom opcodes should range 11 - 239; see http://msdn.microsoft.com/en-us/library/windows/desktop/dd996918(v=vs.85).aspx + public const EventOpcode ReaderWriterLockWaiting = (EventOpcode)100; + public const EventOpcode ReaderWriterLockIssued = (EventOpcode)101; + public const EventOpcode ReaderWriterLockIssuedAfterContention = (EventOpcode)102; } } diff --git a/src/Microsoft.VisualStudio.Threading/ThreadingTools.cs b/src/Microsoft.VisualStudio.Threading/ThreadingTools.cs index 26336abd0..d6d87f01c 100644 --- a/src/Microsoft.VisualStudio.Threading/ThreadingTools.cs +++ b/src/Microsoft.VisualStudio.Threading/ThreadingTools.cs @@ -1,369 +1,368 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// Utility methods for working across threads. +/// +public static class ThreadingTools { - using System; - using System.Threading; - using System.Threading.Tasks; + internal interface ICancellationNotification + { + void OnCanceled(); + } /// - /// Utility methods for working across threads. + /// Optimistically performs some value transformation based on some field and tries to apply it back to the field, + /// retrying as many times as necessary until no other thread is manipulating the same field. /// - public static class ThreadingTools + /// The type of data. + /// The field that may be manipulated by multiple threads. + /// A function that receives the unchanged value and returns the changed value. + /// + /// if the location's value is changed by applying the result of the function; + /// if the location's value remained the same because the last invocation of returned the existing value. + /// + public static bool ApplyChangeOptimistically(ref T hotLocation, Func applyChange) + where T : class? { - internal interface ICancellationNotification - { - void OnCanceled(); - } + Requires.NotNull(applyChange, nameof(applyChange)); - /// - /// Optimistically performs some value transformation based on some field and tries to apply it back to the field, - /// retrying as many times as necessary until no other thread is manipulating the same field. - /// - /// The type of data. - /// The field that may be manipulated by multiple threads. - /// A function that receives the unchanged value and returns the changed value. - /// - /// true if the location's value is changed by applying the result of the function; - /// false if the location's value remained the same because the last invocation of returned the existing value. - /// - public static bool ApplyChangeOptimistically(ref T hotLocation, Func applyChange) - where T : class? + bool successful; + do { - Requires.NotNull(applyChange, nameof(applyChange)); - - bool successful; - do + T oldValue = Volatile.Read(ref hotLocation); + T newValue = applyChange(oldValue); + if (object.ReferenceEquals(oldValue, newValue)) { - T oldValue = Volatile.Read(ref hotLocation); - T newValue = applyChange(oldValue); - if (object.ReferenceEquals(oldValue, newValue)) - { - // No change was actually required. - return false; - } - - T actualOldValue = Interlocked.CompareExchange(ref hotLocation, newValue, oldValue); - successful = object.ReferenceEquals(oldValue, actualOldValue); + // No change was actually required. + return false; } - while (!successful); - return true; + T actualOldValue = Interlocked.CompareExchange(ref hotLocation, newValue, oldValue); + successful = object.ReferenceEquals(oldValue, actualOldValue); } + while (!successful); - /// - /// Optimistically performs some value transformation based on some field and tries to apply it back to the field, - /// retrying as many times as necessary until no other thread is manipulating the same field. - /// - /// - /// Use this overload when requires a single item, as is common when updating immutable - /// collection types. By passing the item as a method operand, the caller may be able to avoid allocating a closure - /// object for every call. - /// - /// The type of data to apply the change to. - /// The type of argument passed to the . - /// The field that may be manipulated by multiple threads. - /// An argument to pass to . - /// A function that receives both the unchanged value and , then returns the changed value. - /// - /// true if the location's value is changed by applying the result of the function; - /// false if the location's value remained the same because the last invocation of returned the existing value. - /// - public static bool ApplyChangeOptimistically(ref T hotLocation, TArg applyChangeArgument, Func applyChange) - where T : class? - { - Requires.NotNull(applyChange, nameof(applyChange)); + return true; + } - bool successful; - do + /// + /// Optimistically performs some value transformation based on some field and tries to apply it back to the field, + /// retrying as many times as necessary until no other thread is manipulating the same field. + /// + /// + /// Use this overload when requires a single item, as is common when updating immutable + /// collection types. By passing the item as a method operand, the caller may be able to avoid allocating a closure + /// object for every call. + /// + /// The type of data to apply the change to. + /// The type of argument passed to the . + /// The field that may be manipulated by multiple threads. + /// An argument to pass to . + /// A function that receives both the unchanged value and , then returns the changed value. + /// + /// if the location's value is changed by applying the result of the function; + /// if the location's value remained the same because the last invocation of returned the existing value. + /// + public static bool ApplyChangeOptimistically(ref T hotLocation, TArg applyChangeArgument, Func applyChange) + where T : class? + { + Requires.NotNull(applyChange, nameof(applyChange)); + + bool successful; + do + { + T oldValue = Volatile.Read(ref hotLocation); + T newValue = applyChange(oldValue, applyChangeArgument); + if (object.ReferenceEquals(oldValue, newValue)) { - T oldValue = Volatile.Read(ref hotLocation); - T newValue = applyChange(oldValue, applyChangeArgument); - if (object.ReferenceEquals(oldValue, newValue)) - { - // No change was actually required. - return false; - } - - T actualOldValue = Interlocked.CompareExchange(ref hotLocation, newValue, oldValue); - successful = object.ReferenceEquals(oldValue, actualOldValue); + // No change was actually required. + return false; } - while (!successful); - return true; + T actualOldValue = Interlocked.CompareExchange(ref hotLocation, newValue, oldValue); + successful = object.ReferenceEquals(oldValue, actualOldValue); } + while (!successful); - /// - /// Wraps a task with one that will complete as cancelled based on a cancellation token, - /// allowing someone to await a task but be able to break out early by cancelling the token. - /// - /// The type of value returned by the task. - /// The task to wrap. - /// The token that can be canceled to break out of the await. - /// The wrapping task. - public static Task WithCancellation(this Task task, CancellationToken cancellationToken) - { - Requires.NotNull(task, nameof(task)); - - if (!cancellationToken.CanBeCanceled || task.IsCompleted) - { - return task; - } + return true; + } - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); - } + /// + /// Wraps a task with one that will complete as cancelled based on a cancellation token, + /// allowing someone to await a task but be able to break out early by cancelling the token. + /// + /// The type of value returned by the task. + /// The task to wrap. + /// The token that can be canceled to break out of the await. + /// The wrapping task. + public static Task WithCancellation(this Task task, CancellationToken cancellationToken) + { + Requires.NotNull(task, nameof(task)); - return WithCancellationSlow(task, cancellationToken); + if (!cancellationToken.CanBeCanceled || task.IsCompleted) + { + return task; } - /// - /// Wraps a task with one that will complete as cancelled based on a cancellation token, - /// allowing someone to await a task but be able to break out early by cancelling the token. - /// - /// The task to wrap. - /// The token that can be canceled to break out of the await. - /// The wrapping task. - public static Task WithCancellation(this Task task, CancellationToken cancellationToken) + if (cancellationToken.IsCancellationRequested) { - Requires.NotNull(task, nameof(task)); + return Task.FromCanceled(cancellationToken); + } - if (!cancellationToken.CanBeCanceled || task.IsCompleted) - { - return task; - } + return WithCancellationSlow(task, cancellationToken); + } - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); - } + /// + /// Wraps a task with one that will complete as cancelled based on a cancellation token, + /// allowing someone to await a task but be able to break out early by cancelling the token. + /// + /// The task to wrap. + /// The token that can be canceled to break out of the await. + /// The wrapping task. + public static Task WithCancellation(this Task task, CancellationToken cancellationToken) + { + Requires.NotNull(task, nameof(task)); - return WithCancellationSlow(task, continueOnCapturedContext: false, cancellationToken: cancellationToken); + if (!cancellationToken.CanBeCanceled || task.IsCompleted) + { + return task; } - /// - /// Applies the specified to the caller's context. - /// - /// The synchronization context to apply. - /// A value indicating whether to check that the applied SyncContext is still the current one when the original is restored. - public static SpecializedSyncContext Apply(this SynchronizationContext? syncContext, bool checkForChangesOnRevert = true) + if (cancellationToken.IsCancellationRequested) { - return SpecializedSyncContext.Apply(syncContext, checkForChangesOnRevert); + return Task.FromCanceled(cancellationToken); } - /// - /// Wraps a task with one that will complete as cancelled based on a cancellation token, - /// allowing someone to await a task but be able to break out early by cancelling the token. - /// - /// The task to wrap. - /// A value indicating whether *internal* continuations required to respond to cancellation should run on the current . - /// The token that can be canceled to break out of the await. - /// The wrapping task. - internal static Task WithCancellation(this Task task, bool continueOnCapturedContext, CancellationToken cancellationToken) - { - Requires.NotNull(task, nameof(task)); + return WithCancellationSlow(task, continueOnCapturedContext: false, cancellationToken: cancellationToken); + } - if (!cancellationToken.CanBeCanceled || task.IsCompleted) - { - return task; - } + /// + /// Applies the specified to the caller's context. + /// + /// The synchronization context to apply. + /// A value indicating whether to check that the applied SyncContext is still the current one when the original is restored. + public static SpecializedSyncContext Apply(this SynchronizationContext? syncContext, bool checkForChangesOnRevert = true) + { + return SpecializedSyncContext.Apply(syncContext, checkForChangesOnRevert); + } - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); - } + /// + /// Wraps a task with one that will complete as cancelled based on a cancellation token, + /// allowing someone to await a task but be able to break out early by cancelling the token. + /// + /// The task to wrap. + /// A value indicating whether *internal* continuations required to respond to cancellation should run on the current . + /// The token that can be canceled to break out of the await. + /// The wrapping task. + internal static Task WithCancellation(this Task task, bool continueOnCapturedContext, CancellationToken cancellationToken) + { + Requires.NotNull(task, nameof(task)); - return WithCancellationSlow(task, continueOnCapturedContext, cancellationToken); + if (!cancellationToken.CanBeCanceled || task.IsCompleted) + { + return task; } - /// - /// Cancels a if a given is canceled. - /// - /// The type of value returned by a successfully completed . - /// The to cancel. - /// The . - /// A callback to invoke when cancellation occurs. - internal static void AttachCancellation(this TaskCompletionSource taskCompletionSource, CancellationToken cancellationToken, ICancellationNotification? cancellationCallback = null) + if (cancellationToken.IsCancellationRequested) { - Requires.NotNull(taskCompletionSource, nameof(taskCompletionSource)); + return Task.FromCanceled(cancellationToken); + } + + return WithCancellationSlow(task, continueOnCapturedContext, cancellationToken); + } + + /// + /// Cancels a if a given is canceled. + /// + /// The type of value returned by a successfully completed . + /// The to cancel. + /// The . + /// A callback to invoke when cancellation occurs. + internal static void AttachCancellation(this TaskCompletionSource taskCompletionSource, CancellationToken cancellationToken, ICancellationNotification? cancellationCallback = null) + { + Requires.NotNull(taskCompletionSource, nameof(taskCompletionSource)); - if (cancellationToken.CanBeCanceled && !taskCompletionSource.Task.IsCompleted) + if (cancellationToken.CanBeCanceled && !taskCompletionSource.Task.IsCompleted) + { + if (cancellationToken.IsCancellationRequested) { - if (cancellationToken.IsCancellationRequested) - { - taskCompletionSource.TrySetCanceled(cancellationToken); - } - else - { - var tuple = new CancelableTaskCompletionSource(taskCompletionSource, cancellationCallback, cancellationToken); - tuple.CancellationTokenRegistration = cancellationToken.Register( - s => + taskCompletionSource.TrySetCanceled(cancellationToken); + } + else + { + var tuple = new CancelableTaskCompletionSource(taskCompletionSource, cancellationCallback, cancellationToken); + tuple.CancellationTokenRegistration = cancellationToken.Register( + s => + { + var t = (CancelableTaskCompletionSource)s!; + if (t.TaskCompletionSource.TrySetCanceled(t.CancellationToken)) + { + t.CancellationCallback?.OnCanceled(); + } + }, + tuple, + useSynchronizationContext: false); + + // In certain race conditions, our continuation could execute inline. We could force it to always run + // asynchronously, but then in the common case it becomes less efficient. + // Instead, we will optimize for the common (no-race) case and detect if we were inlined, and if so, defer the work + // to avoid making our caller block for arbitrary code since CTR.Dispose blocks for in-progress cancellation notification to complete. + taskCompletionSource.Task.ContinueWith( + (_, s) => + { + var t = (CancelableTaskCompletionSource)s!; + if (t.ContinuationScheduled || !t.OnOwnerThread) { - var t = (CancelableTaskCompletionSource)s!; - if (t.TaskCompletionSource.TrySetCanceled(t.CancellationToken)) - { - t.CancellationCallback?.OnCanceled(); - } - }, - tuple, - useSynchronizationContext: false); - - // In certain race conditions, our continuation could execute inline. We could force it to always run - // asynchronously, but then in the common case it becomes less efficient. - // Instead, we will optimize for the common (no-race) case and detect if we were inlined, and if so, defer the work - // to avoid making our caller block for arbitrary code since CTR.Dispose blocks for in-progress cancellation notification to complete. - taskCompletionSource.Task.ContinueWith( - (_, s) => + // We're not executing inline... Go ahead and do the work. + t.CancellationTokenRegistration.Dispose(); + } + else if (!t.CancellationToken.IsCancellationRequested) // If the CT is canceled, the CTR is implicitly disposed. { - var t = (CancelableTaskCompletionSource)s!; - if (t.ContinuationScheduled || !t.OnOwnerThread) - { - // We're not executing inline... Go ahead and do the work. - t.CancellationTokenRegistration.Dispose(); - } - else if (!t.CancellationToken.IsCancellationRequested) // If the CT is canceled, the CTR is implicitly disposed. - { - // We hit the race where the task is already completed another way, - // and our continuation is executing inline with our caller. - // Dispose our CTR from the threadpool to avoid blocking on 3rd party code. - ThreadPool.QueueUserWorkItem( - s2 => + // We hit the race where the task is already completed another way, + // and our continuation is executing inline with our caller. + // Dispose our CTR from the threadpool to avoid blocking on 3rd party code. + ThreadPool.QueueUserWorkItem( + s2 => + { + try { - try - { - var t2 = (CancelableTaskCompletionSource)s2!; - t2.CancellationTokenRegistration.Dispose(); - } - catch (Exception ex) - { - // Swallow any exception. - Report.Fail(ex.Message); - } - }, - s); - } - }, - tuple, - CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default); - tuple.ContinuationScheduled = true; - } + var t2 = (CancelableTaskCompletionSource)s2!; + t2.CancellationTokenRegistration.Dispose(); + } + catch (Exception ex) + { + // Swallow any exception. + Report.Fail(ex.Message); + } + }, + s); + } + }, + tuple, + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + tuple.ContinuationScheduled = true; } } + } - /// - /// Wraps a task with one that will complete as cancelled based on a cancellation token, - /// allowing someone to await a task but be able to break out early by cancelling the token. - /// - /// The type of value returned by the task. - /// The task to wrap. - /// The token that can be canceled to break out of the await. - /// The wrapping task. - private static async Task WithCancellationSlow(Task task, CancellationToken cancellationToken) - { - Assumes.NotNull(task); - Assumes.True(cancellationToken.CanBeCanceled); + /// + /// Wraps a task with one that will complete as cancelled based on a cancellation token, + /// allowing someone to await a task but be able to break out early by cancelling the token. + /// + /// The type of value returned by the task. + /// The task to wrap. + /// The token that can be canceled to break out of the await. + /// The wrapping task. + private static async Task WithCancellationSlow(Task task, CancellationToken cancellationToken) + { + Assumes.NotNull(task); + Assumes.True(cancellationToken.CanBeCanceled); - var tcs = new TaskCompletionSource(); - using (cancellationToken.Register(s => ((TaskCompletionSource)s!).TrySetResult(true), tcs)) + var tcs = new TaskCompletionSource(); + using (cancellationToken.Register(s => ((TaskCompletionSource)s!).TrySetResult(true), tcs)) + { + if (task != await Task.WhenAny(task, tcs.Task).ConfigureAwait(false)) { - if (task != await Task.WhenAny(task, tcs.Task).ConfigureAwait(false)) - { - cancellationToken.ThrowIfCancellationRequested(); - } + cancellationToken.ThrowIfCancellationRequested(); } - - // Rethrow any fault/cancellation exception, even if we awaited above. - // But if we skipped the above if branch, this will actually yield - // on an incompleted task. - return await task.ConfigureAwait(false); } - /// - /// Wraps a task with one that will complete as cancelled based on a cancellation token, - /// allowing someone to await a task but be able to break out early by cancelling the token. - /// - /// The task to wrap. - /// A value indicating whether *internal* continuations required to respond to cancellation should run on the current . - /// The token that can be canceled to break out of the await. - /// The wrapping task. - private static async Task WithCancellationSlow(this Task task, bool continueOnCapturedContext, CancellationToken cancellationToken) - { - Assumes.NotNull(task); - Assumes.True(cancellationToken.CanBeCanceled); + // Rethrow any fault/cancellation exception, even if we awaited above. + // But if we skipped the above if branch, this will actually yield + // on an incompleted task. + return await task.ConfigureAwait(false); + } + + /// + /// Wraps a task with one that will complete as cancelled based on a cancellation token, + /// allowing someone to await a task but be able to break out early by cancelling the token. + /// + /// The task to wrap. + /// A value indicating whether *internal* continuations required to respond to cancellation should run on the current . + /// The token that can be canceled to break out of the await. + /// The wrapping task. + private static async Task WithCancellationSlow(this Task task, bool continueOnCapturedContext, CancellationToken cancellationToken) + { + Assumes.NotNull(task); + Assumes.True(cancellationToken.CanBeCanceled); - var tcs = new TaskCompletionSource(); - using (cancellationToken.Register(s => ((TaskCompletionSource)s!).TrySetResult(true), tcs)) + var tcs = new TaskCompletionSource(); + using (cancellationToken.Register(s => ((TaskCompletionSource)s!).TrySetResult(true), tcs)) + { + if (task != await Task.WhenAny(task, tcs.Task).ConfigureAwait(continueOnCapturedContext)) { - if (task != await Task.WhenAny(task, tcs.Task).ConfigureAwait(continueOnCapturedContext)) - { - cancellationToken.ThrowIfCancellationRequested(); - } + cancellationToken.ThrowIfCancellationRequested(); } - - // Rethrow any fault/cancellation exception, even if we awaited above. - // But if we skipped the above if branch, this will actually yield - // on an incompleted task. - await task.ConfigureAwait(continueOnCapturedContext); } + // Rethrow any fault/cancellation exception, even if we awaited above. + // But if we skipped the above if branch, this will actually yield + // on an incompleted task. + await task.ConfigureAwait(continueOnCapturedContext); + } + + /// + /// A state object for tracking cancellation and a TaskCompletionSource. + /// + /// The type of value returned from a task. + /// + /// We use this class so that we only allocate one object to support all continuations + /// required for cancellation handling, rather than a special closure and delegate for each one. + /// + private class CancelableTaskCompletionSource + { + /// + /// The ID of the thread on which this instance was created. + /// + private readonly int ownerThreadId = Environment.CurrentManagedThreadId; + /// - /// A state object for tracking cancellation and a TaskCompletionSource. + /// Initializes a new instance of the class. /// - /// The type of value returned from a task. - /// - /// We use this class so that we only allocate one object to support all continuations - /// required for cancellation handling, rather than a special closure and delegate for each one. - /// - private class CancelableTaskCompletionSource + /// The task completion source. + /// A callback to invoke when cancellation occurs. + /// The cancellation token. + internal CancelableTaskCompletionSource(TaskCompletionSource taskCompletionSource, ICancellationNotification? cancellationCallback, CancellationToken cancellationToken) { - /// - /// The ID of the thread on which this instance was created. - /// - private readonly int ownerThreadId = Environment.CurrentManagedThreadId; - - /// - /// Initializes a new instance of the class. - /// - /// The task completion source. - /// A callback to invoke when cancellation occurs. - /// The cancellation token. - internal CancelableTaskCompletionSource(TaskCompletionSource taskCompletionSource, ICancellationNotification? cancellationCallback, CancellationToken cancellationToken) - { - this.TaskCompletionSource = taskCompletionSource ?? throw new ArgumentNullException(nameof(taskCompletionSource)); - this.CancellationToken = cancellationToken; - this.CancellationCallback = cancellationCallback; - } + this.TaskCompletionSource = taskCompletionSource ?? throw new ArgumentNullException(nameof(taskCompletionSource)); + this.CancellationToken = cancellationToken; + this.CancellationCallback = cancellationCallback; + } - /// - /// Gets the cancellation token. - /// - internal CancellationToken CancellationToken { get; } + /// + /// Gets the cancellation token. + /// + internal CancellationToken CancellationToken { get; } - /// - /// Gets the Task completion source. - /// - internal TaskCompletionSource TaskCompletionSource { get; } + /// + /// Gets the Task completion source. + /// + internal TaskCompletionSource TaskCompletionSource { get; } - internal ICancellationNotification? CancellationCallback { get; } + internal ICancellationNotification? CancellationCallback { get; } - /// - /// Gets or sets the cancellation token registration. - /// - internal CancellationTokenRegistration CancellationTokenRegistration { get; set; } + /// + /// Gets or sets the cancellation token registration. + /// + internal CancellationTokenRegistration CancellationTokenRegistration { get; set; } - /// - /// Gets or sets a value indicating whether the continuation has been scheduled (and not run inline). - /// - internal bool ContinuationScheduled { get; set; } + /// + /// Gets or sets a value indicating whether the continuation has been scheduled (and not run inline). + /// + internal bool ContinuationScheduled { get; set; } - /// - /// Gets a value indicating whether the caller is on the same thread as the one that created this instance. - /// - internal bool OnOwnerThread => Environment.CurrentManagedThreadId == this.ownerThreadId; - } + /// + /// Gets a value indicating whether the caller is on the same thread as the one that created this instance. + /// + internal bool OnOwnerThread => Environment.CurrentManagedThreadId == this.ownerThreadId; } } diff --git a/src/Microsoft.VisualStudio.Threading/TplExtensions.cs b/src/Microsoft.VisualStudio.Threading/TplExtensions.cs index 88e27ef73..9fdb6aa76 100644 --- a/src/Microsoft.VisualStudio.Threading/TplExtensions.cs +++ b/src/Microsoft.VisualStudio.Threading/TplExtensions.cs @@ -1,867 +1,1110 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Security; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// Extensions to the Task Parallel Library. +/// +public static partial class TplExtensions { - using System; - using System.Collections.Generic; - using System.Runtime.CompilerServices; - using System.Security; - using System.Threading; - using System.Threading.Tasks; + /// + /// A singleton completed task. + /// + [Obsolete("Use Task.CompletedTask instead.")] + public static readonly Task CompletedTask = Task.FromResult(default(EmptyStruct)); + + /// + /// A task that is already canceled. + /// + [Obsolete("Use Task.FromCanceled instead.")] + public static readonly Task CanceledTask = Task.FromCanceled(new CancellationToken(canceled: true)); + + /// + /// A completed task with a result. + /// + public static readonly Task TrueTask = Task.FromResult(true); /// - /// Extensions to the Task Parallel Library. + /// A completed task with a result. /// - public static partial class TplExtensions + public static readonly Task FalseTask = Task.FromResult(false); + + /// + /// Wait on a task without possibly inlining it to the current thread. + /// + /// The task to wait on. + public static void WaitWithoutInlining(this Task task) { - /// - /// A singleton completed task. - /// - [Obsolete("Use Task.CompletedTask instead.")] - public static readonly Task CompletedTask = Task.FromResult(default(EmptyStruct)); + Requires.NotNull(task, nameof(task)); + if (!task.IsCompleted) + { + // Waiting on a continuation of a task won't ever inline the predecessor (in .NET 4.x anyway). + Task? continuation = task.ContinueWith(t => { }, CancellationToken.None, TaskContinuationOptions.RunContinuationsAsynchronously, TaskScheduler.Default); + continuation.Wait(); + } - /// - /// A task that is already canceled. - /// - [Obsolete("Use Task.FromCanceled instead.")] - public static readonly Task CanceledTask = Task.FromCanceled(new CancellationToken(canceled: true)); + task.Wait(); // purely for exception behavior; alternatively in .NET 4.5 task.GetAwaiter().GetResult(); + } + + /// + /// Returns a task that completes as the original task completes or when a timeout expires, + /// whichever happens first. + /// + /// The task to wait for. + /// The maximum time to wait. + /// + /// A task that completes with the result of the specified or + /// faults with a if elapses first. + /// + public static async Task WithTimeout(this Task task, TimeSpan timeout) + { + Requires.NotNull(task, nameof(task)); + + using (var timerCancellation = new CancellationTokenSource()) + { + Task timeoutTask = Task.Delay(timeout, timerCancellation.Token); + Task firstCompletedTask = await Task.WhenAny(task, timeoutTask).ConfigureAwait(false); + if (firstCompletedTask == timeoutTask) + { + throw new TimeoutException(); + } + + // The timeout did not elapse, so cancel the timer to recover system resources. + timerCancellation.Cancel(); + + // re-throw any exceptions from the completed task. + await task.ConfigureAwait(false); + } + } + + /// + /// Returns a task that completes as the original task completes or when a timeout expires, + /// whichever happens first. + /// + /// The type of value returned by the original task. + /// The task to wait for. + /// The maximum time to wait. + /// + /// A task that completes with the result of the specified or + /// faults with a if elapses first. + /// + public static async Task WithTimeout(this Task task, TimeSpan timeout) + { + await WithTimeout((Task)task, timeout).ConfigureAwait(false); + return task.GetAwaiter().GetResult(); + } + + /// + /// Applies one task's results to another. + /// + /// The type of value returned by a task. + /// The task whose completion should be applied to another. + /// The task that should receive the completion status. + public static void ApplyResultTo(this Task task, TaskCompletionSource tcs) + { + ApplyResultTo(task, tcs, inlineSubsequentCompletion: true); + } + + /// + /// Applies one task's results to another. + /// + /// The type of value returned by a task. + /// The task whose completion should be applied to another. + /// The task that should receive the completion status. + public static void ApplyResultTo(this Task task, TaskCompletionSource tcs) + //// where T : defaultable + { + Requires.NotNull(task, nameof(task)); + Requires.NotNull(tcs, nameof(tcs)); + + if (task.IsCompleted) + { + ApplyCompletedTaskResultTo(task, tcs, default(T)!); + } + else + { + // Using a minimum of allocations (just one task, and no closure) ensure that one task's completion sets equivalent completion on another task. + task.ContinueWith( + (t, s) => ApplyCompletedTaskResultTo(t, (TaskCompletionSource)s!, default(T)!), + tcs, + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } + } + + /// + /// Creates a task that is attached to the parent task, but produces the same result as an existing task. + /// + /// The type of value produced by the task. + /// The task to wrap with an AttachedToParent task. + /// A task that is attached to parent. + public static Task AttachToParent(this Task task) + { + Requires.NotNull(task, nameof(task)); + + var tcs = new TaskCompletionSource(TaskCreationOptions.AttachedToParent); + task.ApplyResultTo(tcs); + return tcs.Task; + } + + /// + /// Creates a task that is attached to the parent task, but produces the same result as an existing task. + /// + /// The task to wrap with an AttachedToParent task. + /// A task that is attached to parent. + public static Task AttachToParent(this Task task) + { + Requires.NotNull(task, nameof(task)); + + var tcs = new TaskCompletionSource(TaskCreationOptions.AttachedToParent); + task.ApplyResultTo(tcs); + return tcs.Task; + } + + /// + /// Schedules some action for execution at the conclusion of a task, regardless of the task's outcome. + /// + /// The task that should complete before the posted is invoked. + /// The action to execute after has completed. + /// The task continuation options to apply. + /// The cancellation token that signals the continuation should not execute (if it has not already begun). + /// + /// The task that will execute the action. + /// + public static Task AppendAction(this Task task, Action action, TaskContinuationOptions options = TaskContinuationOptions.None, CancellationToken cancellation = default(CancellationToken)) + { + Requires.NotNull(task, nameof(task)); + Requires.NotNull(action, nameof(action)); + + return task.ContinueWith((t, state) => ((Action)state!)(), action, cancellation, options, TaskScheduler.Default); + } + + /// + /// Gets a task that will eventually produce the result of another task, when that task finishes. + /// If that task is instead canceled, its successor will be followed for its result, iteratively. + /// + /// The type of value returned by the task. + /// The task whose result should be returned by the following task. + /// A token whose cancellation signals that the following task should be cancelled. + /// The TaskCompletionSource whose task is to follow. Leave at for a new task to be created. + /// The following task. + public static Task FollowCancelableTaskToCompletion(Func> taskToFollow, CancellationToken ultimateCancellation, TaskCompletionSource? taskThatFollows = null) + { + Requires.NotNull(taskToFollow, nameof(taskToFollow)); + + var tcs = new TaskCompletionSource, T>( + new FollowCancelableTaskState(taskToFollow, ultimateCancellation)); + + if (ultimateCancellation.CanBeCanceled) + { + CancellationTokenRegistration registeredCallback = ultimateCancellation.Register( + state => + { + var tuple = (Tuple, T>, CancellationToken>)state!; + tuple.Item1.TrySetCanceled(tuple.Item2); + }, + Tuple.Create(tcs, ultimateCancellation)); + tcs.SourceState = tcs.SourceState.WithRegisteredCallback(registeredCallback); + } + + FollowCancelableTaskToCompletionHelper(tcs, taskToFollow()); + + if (taskThatFollows is null) + { + return tcs.Task; + } + else + { + tcs.Task.ApplyResultTo(taskThatFollows); + return taskThatFollows.Task; + } + } + + /// + /// Returns an awaitable for the specified task that will never throw, even if the source task + /// faults or is canceled. + /// + /// The task whose completion should signal the completion of the returned awaitable. + /// if set to the continuation will be scheduled on the caller's context; to always execute the continuation on the threadpool. + /// An awaitable. + public static NoThrowTaskAwaitable NoThrowAwaitable(this Task task, bool captureContext = true) + { + return new NoThrowTaskAwaitable(task, captureContext); + } + + /// + /// Returns an awaitable for the specified task that will never throw, even if the source task + /// faults or is canceled. + /// + /// The task whose completion should signal the completion of the returned awaitable. + /// if set to the continuation will be scheduled on the caller's context; to always execute the continuation on the threadpool. + /// An awaitable. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "The receiver type is disjoint.")] + public static NoThrowValueTaskAwaitable NoThrowAwaitable(this ValueTask task, bool captureContext = true) + { + return new NoThrowValueTaskAwaitable(task, captureContext); + } + + /// + /// Returns an awaitable for the specified task that will never throw, even if the source task + /// faults or is canceled. + /// + /// + /// The awaitable returned by this method does not provide access to the result of a successfully-completed + /// . To await without throwing and use the resulting value, the following + /// pattern may be used: + /// + /// + /// var methodValueTask = MethodAsync().Preserve(); + /// await methodValueTask.NoThrowAwaitable(true); + /// if (methodValueTask.IsCompletedSuccessfully) + /// { + /// var result = methodValueTask.Result; + /// } + /// else + /// { + /// var exception = methodValueTask.AsTask().Exception.InnerException; + /// } + /// + /// + /// The task whose completion should signal the completion of the returned awaitable. + /// if set to the continuation will be scheduled on the caller's context; to always execute the continuation on the threadpool. + /// An awaitable. + /// The type of the result. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "The receiver type is disjoint.")] + public static NoThrowValueTaskAwaitable NoThrowAwaitable(this ValueTask task, bool captureContext = true) + { + return new NoThrowValueTaskAwaitable(task, captureContext); + } + + /// + /// Consumes a task and doesn't do anything with it. Useful for fire-and-forget calls to async methods within async methods. + /// + /// The task whose result is to be ignored. + public static void Forget(this Task? task) + { + } + + /// + /// Consumes a and allows it to be recycled, if applicable. Useful for fire-and-forget calls to async methods within async methods. + /// NOTE: APIs should not generally return if callers aren't 99.9999% likely to await the result immediately. + /// + /// The task whose result is to be ignored. + public static void Forget(this ValueTask task) => task.Preserve(); + + /// + /// Consumes a ValueTask and allows it to be recycled, if applicable. Useful for fire-and-forget calls to async methods within async methods. + /// NOTE: APIs should not generally return if callers aren't 99.9999% likely to await the result immediately. + /// + /// The type of value produced by the . + /// The task whose result is to be ignored. + public static void Forget(this ValueTask task) => task.Preserve(); + + /// + /// Invokes asynchronous event handlers, returning a task that completes when all event handlers have been invoked. + /// Each handler is fully executed (including continuations) before the next handler in the list is invoked. + /// + /// The event handlers. May be . + /// The event source. + /// The event argument. + /// The task that completes when all handlers have completed. + /// Thrown if any handlers fail. It contains a collection of all failures. + public static async Task InvokeAsync(this AsyncEventHandler? handlers, object? sender, EventArgs args) + { + if (handlers is object) + { + Delegate[]? individualHandlers = handlers.GetInvocationList(); + List? exceptions = null; + foreach (AsyncEventHandler handler in individualHandlers) + { + try + { + await handler(sender, args).ConfigureAwait(true); + } + catch (Exception ex) + { + if (exceptions is null) + { + exceptions = new List(2); + } + + exceptions.Add(ex); + } + } + + if (exceptions is object) + { + throw new AggregateException(exceptions); + } + } + } + + /// + /// Invokes asynchronous event handlers, returning a task that completes when all event handlers have been invoked. + /// Each handler is fully executed (including continuations) before the next handler in the list is invoked. + /// + /// The type of argument passed to each handler. + /// The event handlers. May be . + /// The event source. + /// The event argument. + /// The task that completes when all handlers have completed. The task is faulted if any handlers throw an exception. + /// Thrown if any handlers fail. It contains a collection of all failures. + public static async Task InvokeAsync(this AsyncEventHandler? handlers, object? sender, TEventArgs args) + { + if (handlers is object) + { + Delegate[]? individualHandlers = handlers.GetInvocationList(); + List? exceptions = null; + foreach (AsyncEventHandler handler in individualHandlers) + { + try + { + await handler(sender, args).ConfigureAwait(true); + } + catch (Exception ex) + { + if (exceptions is null) + { + exceptions = new List(2); + } + + exceptions.Add(ex); + } + } + + if (exceptions is object) + { + throw new AggregateException(exceptions); + } + } + } + + /// + /// Converts a TPL task to the APM Begin-End pattern. + /// + /// The result value to be returned from the End method. + /// The task that came from the async method. + /// The optional callback to invoke when the task is completed. + /// The state object provided by the caller of the Begin method. + /// A task (that implements that should be returned from the Begin method. + public static Task ToApm(this Task task, AsyncCallback? callback, object? state) + { + Requires.NotNull(task, nameof(task)); + + if (task.AsyncState == state) + { + if (callback is object) + { + task.ContinueWith( + (t, cb) => ((AsyncCallback)cb!)(t), + callback, + CancellationToken.None, + TaskContinuationOptions.None, + TaskScheduler.Default); + } + + return task; + } + + var tcs = new TaskCompletionSource(state); + task.ContinueWith( + t => + { + ApplyCompletedTaskResultTo(t, tcs); + + callback?.Invoke(tcs.Task); + }, + CancellationToken.None, + TaskContinuationOptions.None, + TaskScheduler.Default); + + return tcs.Task; + } + + /// + /// Converts a TPL task to the APM Begin-End pattern. + /// + /// The task that came from the async method. + /// The optional callback to invoke when the task is completed. + /// The state object provided by the caller of the Begin method. + /// A task (that implements that should be returned from the Begin method. + public static Task ToApm(this Task task, AsyncCallback? callback, object? state) + { + Requires.NotNull(task, nameof(task)); + + if (task.AsyncState == state) + { + if (callback is object) + { + task.ContinueWith( + (t, cb) => ((AsyncCallback)cb!)(t), + callback, + CancellationToken.None, + TaskContinuationOptions.None, + TaskScheduler.Default); + } + + return task; + } + + var tcs = new TaskCompletionSource(state); + task.ContinueWith( + t => + { + ApplyCompletedTaskResultTo(t, tcs, null); + + callback?.Invoke(tcs.Task); + }, + CancellationToken.None, + TaskContinuationOptions.None, + TaskScheduler.Default); + + return tcs.Task; + } + + /// + /// Creates a TPL Task that returns when a is signaled or returns if a timeout occurs first. + /// + /// The handle whose signal triggers the task to be completed. Do not use a here. + /// The timeout (in milliseconds) after which the task will return if the handle is not signaled by that time. + /// A token whose cancellation will cause the returned Task to immediately complete in a canceled state. + /// + /// A Task that completes when the handle is signaled or times out, or when the caller's cancellation token is canceled. + /// If the task completes because the handle is signaled, the task's result is . + /// If the task completes because the handle is not signaled prior to the timeout, the task's result is . + /// + /// + /// The completion of the returned task is asynchronous with respect to the code that actually signals the wait handle. + /// + public static Task ToTask(this WaitHandle handle, int timeout = Timeout.Infinite, CancellationToken cancellationToken = default(CancellationToken)) + { + Requires.NotNull(handle, nameof(handle)); + + // Check whether the handle is already signaled as an optimization. + // But even for WaitOne(0) the CLR can pump messages if called on the UI thread, which the caller may not + // be expecting at this time, so be sure there is no message pump active by controlling the SynchronizationContext. + using (NoMessagePumpSyncContext.Default.Apply()) + { + if (handle.WaitOne(0)) + { + return TrueTask; + } + else if (timeout == 0) + { + return FalseTask; + } + } + + cancellationToken.ThrowIfCancellationRequested(); + var tcs = new TaskCompletionSource(); + + RegisteredWaitHandle callbackHandle = ThreadPool.RegisterWaitForSingleObject( + handle, + static (state, timedOut) => ((TaskCompletionSource)state!).TrySetResult(!timedOut), + state: tcs, + millisecondsTimeOutInterval: timeout, + executeOnlyOnce: true); + + if (cancellationToken.CanBeCanceled) + { + // Arrange that if the caller signals their cancellation token that we complete the task + // we return immediately. Because of the continuation we've scheduled on that task, this + // will automatically release the wait handle notification as well. + CancellationTokenRegistration cancellationRegistration = + cancellationToken.Register( + static state => + { + var tuple = (Tuple, CancellationToken>)state!; + tuple.Item1.TrySetCanceled(tuple.Item2); + }, + Tuple.Create(tcs, cancellationToken)); + + // We have a cancellation token registration and a wait handle registration to release. + // Each time this code executes, allocate one tuple as a state object to reduce from allocating an implicit closure *and* a delegate. + tcs.Task.ContinueWith( + static (_, state) => + { + var tuple = (Tuple)state!; + tuple.Item1.Unregister(null); // release resources for the async callback + tuple.Item2.Dispose(); // release memory for cancellation token registration + }, + Tuple.Create(callbackHandle, cancellationRegistration), + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } + else + { + // Since the cancellation token was the default one, the only thing we need to track is clearing the RegisteredWaitHandle, + // so do this such that we allocate as few objects as possible. + tcs.Task.ContinueWith( + static (_, state) => ((RegisteredWaitHandle)state!).Unregister(null), + callbackHandle, + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } + + return tcs.Task; + } + + /// + /// Applies one task's results to another. + /// + /// The type of value returned by a task. + /// The task whose completion should be applied to another. + /// The task that should receive the completion status. + /// + /// to complete the supplied as efficiently as possible (inline with the completion of ); + /// to complete the asynchronously. + /// Note if is completed when this method is invoked, then is always completed synchronously. + /// + internal static void ApplyResultTo(this Task task, TaskCompletionSource tcs, bool inlineSubsequentCompletion) + { + Requires.NotNull(task, nameof(task)); + Requires.NotNull(tcs, nameof(tcs)); + + if (task.IsCompleted) + { + ApplyCompletedTaskResultTo(task, tcs); + } + else + { + // Using a minimum of allocations (just one task, and no closure) ensure that one task's completion sets equivalent completion on another task. + task.ContinueWith( + (t, s) => ApplyCompletedTaskResultTo(t, (TaskCompletionSource)s!), + tcs, + CancellationToken.None, + inlineSubsequentCompletion ? TaskContinuationOptions.ExecuteSynchronously : TaskContinuationOptions.None, + TaskScheduler.Default); + } + } + + /// + /// Returns a reusable task that is already canceled. + /// + /// The type parameter for the returned task. + internal static Task CanceledTaskOfT() => CanceledTaskOfTCache.CanceledTask; + + /// + /// Returns a that has been faulted with the specified exception. + /// + /// The type of value that might have been returned from the . + /// The exception used to fault the . + /// The faulted task. + internal static Task FaultedTask(Exception ex) + { + return Task.FromException(ex); + } + + /// + /// Applies a completed task's results to another. + /// + /// The type of value returned by a task. + /// The task whose completion should be applied to another. + /// The task that should receive the completion status. + private static void ApplyCompletedTaskResultTo(Task completedTask, TaskCompletionSource taskCompletionSource) + { + Assumes.NotNull(completedTask); + Assumes.True(completedTask.IsCompleted); + Assumes.NotNull(taskCompletionSource); + + if (completedTask.IsCanceled) + { + // NOTE: this is "lossy" in that we don't propagate any CancellationToken that the Task would throw an OperationCanceledException with. + // Propagating that data would require that we actually cause the completedTask to throw so we can inspect the + // OperationCanceledException.CancellationToken property, which we consider more costly than it's worth. + taskCompletionSource.TrySetCanceled(); + } + else if (completedTask.IsFaulted) + { + taskCompletionSource.TrySetException(completedTask.Exception!.InnerExceptions); + } + else + { + taskCompletionSource.TrySetResult(completedTask.Result); + } + } + + /// + /// Applies a completed task's results to another. + /// + /// The type of value returned by a task. + /// The task whose completion should be applied to another. + /// The task that should receive the completion status. + /// The value to set on the completion source when the source task runs to completion. + private static void ApplyCompletedTaskResultTo(Task completedTask, TaskCompletionSource taskCompletionSource, T valueOnRanToCompletion) + { + Assumes.NotNull(completedTask); + Assumes.True(completedTask.IsCompleted); + Assumes.NotNull(taskCompletionSource); + + if (completedTask.IsCanceled) + { + // NOTE: this is "lossy" in that we don't propagate any CancellationToken that the Task would throw an OperationCanceledException with. + // Propagating that data would require that we actually cause the completedTask to throw so we can inspect the + // OperationCanceledException.CancellationToken property, which we consider more costly than it's worth. + taskCompletionSource.TrySetCanceled(); + } + else if (completedTask.IsFaulted) + { + taskCompletionSource.TrySetException(completedTask.Exception!.InnerExceptions); + } + else + { + taskCompletionSource.TrySetResult(valueOnRanToCompletion); + } + } + + /// + /// Gets a task that will eventually produce the result of another task, when that task finishes. + /// If that task is instead canceled, its successor will be followed for its result, iteratively. + /// + /// The type of value returned by the task. + /// The TaskCompletionSource whose task is to follow. + /// The current task. + /// + /// The following task. + /// + private static Task FollowCancelableTaskToCompletionHelper(TaskCompletionSource, T> tcs, Task currentTask) + { + Requires.NotNull(tcs, nameof(tcs)); + Requires.NotNull(currentTask, nameof(currentTask)); + + currentTask.ContinueWith( + (t, state) => + { + var tcsNested = (TaskCompletionSource, T>)state!; + switch (t.Status) + { + case TaskStatus.RanToCompletion: + tcsNested.TrySetResult(t.Result); + tcsNested.SourceState.RegisteredCallback.Dispose(); + break; + case TaskStatus.Faulted: + tcsNested.TrySetException(t.Exception!.InnerExceptions); + tcsNested.SourceState.RegisteredCallback.Dispose(); + break; + case TaskStatus.Canceled: + Task? newTask = tcsNested.SourceState.CurrentTask; + Assumes.True(newTask != t, "A canceled task was not replaced with a new task."); + FollowCancelableTaskToCompletionHelper(tcsNested, newTask); + break; + } + }, + tcs, + tcs.SourceState.UltimateCancellation, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + + return tcs.Task; + } + /// + /// An awaitable that wraps a task and never throws an exception when waited on. + /// + public readonly struct NoThrowTaskAwaitable + { /// - /// A completed task with a true result. + /// The task. /// - public static readonly Task TrueTask = Task.FromResult(true); + private readonly Task task; /// - /// A completed task with a false result. + /// A value indicating whether the continuation should be scheduled on the current sync context. /// - public static readonly Task FalseTask = Task.FromResult(false); + private readonly bool captureContext; /// - /// Wait on a task without possibly inlining it to the current thread. + /// Initializes a new instance of the struct. /// - /// The task to wait on. - public static void WaitWithoutInlining(this Task task) + /// The task. + /// Whether the continuation should be scheduled on the current sync context. + public NoThrowTaskAwaitable(Task task, bool captureContext) { Requires.NotNull(task, nameof(task)); - if (!task.IsCompleted) - { - // Waiting on a continuation of a task won't ever inline the predecessor (in .NET 4.x anyway). - Task? continuation = task.ContinueWith(t => { }, CancellationToken.None, TaskContinuationOptions.RunContinuationsAsynchronously, TaskScheduler.Default); - continuation.Wait(); - } - - task.Wait(); // purely for exception behavior; alternatively in .NET 4.5 task.GetAwaiter().GetResult(); + this.task = task; + this.captureContext = captureContext; } /// - /// Returns a task that completes as the original task completes or when a timeout expires, - /// whichever happens first. + /// Gets the awaiter. /// - /// The task to wait for. - /// The maximum time to wait. - /// - /// A task that completes with the result of the specified or - /// faults with a if elapses first. - /// - public static async Task WithTimeout(this Task task, TimeSpan timeout) + /// The awaiter. + public NoThrowTaskAwaiter GetAwaiter() { - Requires.NotNull(task, nameof(task)); - - using (var timerCancellation = new CancellationTokenSource()) - { - Task timeoutTask = Task.Delay(timeout, timerCancellation.Token); - Task firstCompletedTask = await Task.WhenAny(task, timeoutTask).ConfigureAwait(false); - if (firstCompletedTask == timeoutTask) - { - throw new TimeoutException(); - } - - // The timeout did not elapse, so cancel the timer to recover system resources. - timerCancellation.Cancel(); - - // re-throw any exceptions from the completed task. - await task.ConfigureAwait(false); - } + return new NoThrowTaskAwaiter(this.task, this.captureContext); } + } + /// + /// An awaiter that wraps a task and never throws an exception when waited on. + /// + public readonly struct NoThrowTaskAwaiter : ICriticalNotifyCompletion + { /// - /// Returns a task that completes as the original task completes or when a timeout expires, - /// whichever happens first. + /// The task. /// - /// The type of value returned by the original task. - /// The task to wait for. - /// The maximum time to wait. - /// - /// A task that completes with the result of the specified or - /// faults with a if elapses first. - /// - public static async Task WithTimeout(this Task task, TimeSpan timeout) - { - await WithTimeout((Task)task, timeout).ConfigureAwait(false); - return task.GetAwaiter().GetResult(); - } + private readonly Task task; /// - /// Applies one task's results to another. + /// A value indicating whether the continuation should be scheduled on the current sync context. /// - /// The type of value returned by a task. - /// The task whose completion should be applied to another. - /// The task that should receive the completion status. - public static void ApplyResultTo(this Task task, TaskCompletionSource tcs) - { - ApplyResultTo(task, tcs, inlineSubsequentCompletion: true); - } + private readonly bool captureContext; /// - /// Applies one task's results to another. + /// Initializes a new instance of the struct. /// - /// The type of value returned by a task. - /// The task whose completion should be applied to another. - /// The task that should receive the completion status. - public static void ApplyResultTo(this Task task, TaskCompletionSource tcs) - //// where T : defaultable + /// The task. + /// if set to [capture context]. + public NoThrowTaskAwaiter(Task task, bool captureContext) { Requires.NotNull(task, nameof(task)); - Requires.NotNull(tcs, nameof(tcs)); - - if (task.IsCompleted) - { - ApplyCompletedTaskResultTo(task, tcs, default(T)!); - } - else - { - // Using a minimum of allocations (just one task, and no closure) ensure that one task's completion sets equivalent completion on another task. - task.ContinueWith( - (t, s) => ApplyCompletedTaskResultTo(t, (TaskCompletionSource)s!, default(T)!), - tcs, - CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default); - } + this.task = task; + this.captureContext = captureContext; } /// - /// Creates a task that is attached to the parent task, but produces the same result as an existing task. + /// Gets a value indicating whether the task has completed. /// - /// The type of value produced by the task. - /// The task to wrap with an AttachedToParent task. - /// A task that is attached to parent. - public static Task AttachToParent(this Task task) + public bool IsCompleted { - Requires.NotNull(task, nameof(task)); - - var tcs = new TaskCompletionSource(TaskCreationOptions.AttachedToParent); - task.ApplyResultTo(tcs); - return tcs.Task; + get { return this.task.IsCompleted; } } /// - /// Creates a task that is attached to the parent task, but produces the same result as an existing task. + /// Schedules a delegate for execution at the conclusion of a task's execution. /// - /// The task to wrap with an AttachedToParent task. - /// A task that is attached to parent. - public static Task AttachToParent(this Task task) + /// The action. + public void OnCompleted(Action continuation) { - Requires.NotNull(task, nameof(task)); - - var tcs = new TaskCompletionSource(TaskCreationOptions.AttachedToParent); - task.ApplyResultTo(tcs); - return tcs.Task; + this.task.ConfigureAwait(this.captureContext).GetAwaiter().OnCompleted(continuation); } /// - /// Schedules some action for execution at the conclusion of a task, regardless of the task's outcome. + /// Schedules a delegate for execution at the conclusion of a task's execution + /// without capturing the ExecutionContext. /// - /// The task that should complete before the posted is invoked. - /// The action to execute after has completed. - /// The task continuation options to apply. - /// The cancellation token that signals the continuation should not execute (if it has not already begun). - /// - /// The task that will execute the action. - /// - public static Task AppendAction(this Task task, Action action, TaskContinuationOptions options = TaskContinuationOptions.None, CancellationToken cancellation = default(CancellationToken)) + /// The action. + public void UnsafeOnCompleted(Action continuation) { - Requires.NotNull(task, nameof(task)); - Requires.NotNull(action, nameof(action)); - - return task.ContinueWith((t, state) => ((Action)state!)(), action, cancellation, options, TaskScheduler.Default); + this.task.ConfigureAwait(this.captureContext).GetAwaiter().UnsafeOnCompleted(continuation); } /// - /// Gets a task that will eventually produce the result of another task, when that task finishes. - /// If that task is instead canceled, its successor will be followed for its result, iteratively. + /// Does nothing. /// - /// The type of value returned by the task. - /// The task whose result should be returned by the following task. - /// A token whose cancellation signals that the following task should be cancelled. - /// The TaskCompletionSource whose task is to follow. Leave at null for a new task to be created. - /// The following task. - public static Task FollowCancelableTaskToCompletion(Func> taskToFollow, CancellationToken ultimateCancellation, TaskCompletionSource? taskThatFollows = null) + public void GetResult() { - Requires.NotNull(taskToFollow, nameof(taskToFollow)); - - var tcs = new TaskCompletionSource, T>( - new FollowCancelableTaskState(taskToFollow, ultimateCancellation)); - - if (ultimateCancellation.CanBeCanceled) - { - CancellationTokenRegistration registeredCallback = ultimateCancellation.Register( - state => - { - var tuple = (Tuple, T>, CancellationToken>)state!; - tuple.Item1.TrySetCanceled(tuple.Item2); - }, - Tuple.Create(tcs, ultimateCancellation)); - tcs.SourceState = tcs.SourceState.WithRegisteredCallback(registeredCallback); - } + // Never throw here. + } + } - FollowCancelableTaskToCompletionHelper(tcs, taskToFollow()); + /// + /// An awaitable that wraps a task and never throws an exception when waited on. + /// + public readonly struct NoThrowValueTaskAwaitable + { + /// + /// The task. + /// + private readonly ValueTask task; - if (taskThatFollows is null) - { - return tcs.Task; - } - else - { - tcs.Task.ApplyResultTo(taskThatFollows); - return taskThatFollows.Task; - } - } + /// + /// A value indicating whether the continuation should be scheduled on the current sync context. + /// + private readonly bool captureContext; /// - /// Returns an awaitable for the specified task that will never throw, even if the source task - /// faults or is canceled. + /// Initializes a new instance of the struct. /// - /// The task whose completion should signal the completion of the returned awaitable. - /// if set to true the continuation will be scheduled on the caller's context; false to always execute the continuation on the threadpool. - /// An awaitable. - public static NoThrowTaskAwaitable NoThrowAwaitable(this Task task, bool captureContext = true) + /// The task. + /// Whether the continuation should be scheduled on the current sync context. + public NoThrowValueTaskAwaitable(ValueTask task, bool captureContext) { - return new NoThrowTaskAwaitable(task, captureContext); + this.task = task.Preserve(); + this.captureContext = captureContext; } /// - /// Consumes a task and doesn't do anything with it. Useful for fire-and-forget calls to async methods within async methods. + /// Gets the awaiter. /// - /// The task whose result is to be ignored. - public static void Forget(this Task? task) + /// The awaiter. + public NoThrowValueTaskAwaiter GetAwaiter() { + return new NoThrowValueTaskAwaiter(this.task, this.captureContext); } + } + /// + /// An awaiter that wraps a task and never throws an exception when waited on. + /// + public readonly struct NoThrowValueTaskAwaiter : ICriticalNotifyCompletion + { /// - /// Consumes a and allows it to be recycled, if applicable. Useful for fire-and-forget calls to async methods within async methods. - /// NOTE: APIs should not generally return if callers aren't 99.9999% likely to await the result immediately. + /// The task. /// - /// The task whose result is to be ignored. - public static void Forget(this ValueTask task) => task.Preserve(); + private readonly ValueTask task; /// - /// Consumes a ValueTask and allows it to be recycled, if applicable. Useful for fire-and-forget calls to async methods within async methods. - /// NOTE: APIs should not generally return if callers aren't 99.9999% likely to await the result immediately. + /// A value indicating whether the continuation should be scheduled on the current sync context. /// - /// The type of value produced by the . - /// The task whose result is to be ignored. - public static void Forget(this ValueTask task) => task.Preserve(); + private readonly bool captureContext; /// - /// Invokes asynchronous event handlers, returning a task that completes when all event handlers have been invoked. - /// Each handler is fully executed (including continuations) before the next handler in the list is invoked. + /// Initializes a new instance of the struct. /// - /// The event handlers. May be null. - /// The event source. - /// The event argument. - /// The task that completes when all handlers have completed. - /// Thrown if any handlers fail. It contains a collection of all failures. - public static async Task InvokeAsync(this AsyncEventHandler? handlers, object? sender, EventArgs args) + /// The task. + /// if set to [capture context]. + public NoThrowValueTaskAwaiter(ValueTask task, bool captureContext) { - if (handlers is object) - { - Delegate[]? individualHandlers = handlers.GetInvocationList(); - List? exceptions = null; - foreach (AsyncEventHandler handler in individualHandlers) - { - try - { - await handler(sender, args).ConfigureAwait(true); - } - catch (Exception ex) - { - if (exceptions is null) - { - exceptions = new List(2); - } - - exceptions.Add(ex); - } - } - - if (exceptions is object) - { - throw new AggregateException(exceptions); - } - } + this.task = task; + this.captureContext = captureContext; } /// - /// Invokes asynchronous event handlers, returning a task that completes when all event handlers have been invoked. - /// Each handler is fully executed (including continuations) before the next handler in the list is invoked. + /// Gets a value indicating whether the task has completed. /// - /// The type of argument passed to each handler. - /// The event handlers. May be null. - /// The event source. - /// The event argument. - /// The task that completes when all handlers have completed. The task is faulted if any handlers throw an exception. - /// Thrown if any handlers fail. It contains a collection of all failures. - public static async Task InvokeAsync(this AsyncEventHandler? handlers, object? sender, TEventArgs args) + public bool IsCompleted { - if (handlers is object) - { - Delegate[]? individualHandlers = handlers.GetInvocationList(); - List? exceptions = null; - foreach (AsyncEventHandler handler in individualHandlers) - { - try - { - await handler(sender, args).ConfigureAwait(true); - } - catch (Exception ex) - { - if (exceptions is null) - { - exceptions = new List(2); - } - - exceptions.Add(ex); - } - } - - if (exceptions is object) - { - throw new AggregateException(exceptions); - } - } + get { return this.task.IsCompleted; } } /// - /// Converts a TPL task to the APM Begin-End pattern. + /// Schedules a delegate for execution at the conclusion of a task's execution. /// - /// The result value to be returned from the End method. - /// The task that came from the async method. - /// The optional callback to invoke when the task is completed. - /// The state object provided by the caller of the Begin method. - /// A task (that implements that should be returned from the Begin method. - public static Task ToApm(this Task task, AsyncCallback? callback, object? state) + /// The action. + public void OnCompleted(Action continuation) { - Requires.NotNull(task, nameof(task)); - - if (task.AsyncState == state) - { - if (callback is object) - { - task.ContinueWith( - (t, cb) => ((AsyncCallback)cb!)(t), - callback, - CancellationToken.None, - TaskContinuationOptions.None, - TaskScheduler.Default); - } - - return task; - } - - var tcs = new TaskCompletionSource(state); - task.ContinueWith( - t => - { - ApplyCompletedTaskResultTo(t, tcs); - - callback?.Invoke(tcs.Task); - }, - CancellationToken.None, - TaskContinuationOptions.None, - TaskScheduler.Default); - - return tcs.Task; + this.task.ConfigureAwait(this.captureContext).GetAwaiter().OnCompleted(continuation); } /// - /// Converts a TPL task to the APM Begin-End pattern. + /// Schedules a delegate for execution at the conclusion of a task's execution + /// without capturing the ExecutionContext. /// - /// The task that came from the async method. - /// The optional callback to invoke when the task is completed. - /// The state object provided by the caller of the Begin method. - /// A task (that implements that should be returned from the Begin method. - public static Task ToApm(this Task task, AsyncCallback? callback, object? state) + /// The action. + public void UnsafeOnCompleted(Action continuation) { - Requires.NotNull(task, nameof(task)); - - if (task.AsyncState == state) - { - if (callback is object) - { - task.ContinueWith( - (t, cb) => ((AsyncCallback)cb!)(t), - callback, - CancellationToken.None, - TaskContinuationOptions.None, - TaskScheduler.Default); - } - - return task; - } - - var tcs = new TaskCompletionSource(state); - task.ContinueWith( - t => - { - ApplyCompletedTaskResultTo(t, tcs, null); - - callback?.Invoke(tcs.Task); - }, - CancellationToken.None, - TaskContinuationOptions.None, - TaskScheduler.Default); - - return tcs.Task; + this.task.ConfigureAwait(this.captureContext).GetAwaiter().UnsafeOnCompleted(continuation); } /// - /// Creates a TPL Task that returns true when a is signaled or returns false if a timeout occurs first. + /// Does nothing. /// - /// The handle whose signal triggers the task to be completed. Do not use a here. - /// The timeout (in milliseconds) after which the task will return false if the handle is not signaled by that time. - /// A token whose cancellation will cause the returned Task to immediately complete in a canceled state. - /// - /// A Task that completes when the handle is signaled or times out, or when the caller's cancellation token is canceled. - /// If the task completes because the handle is signaled, the task's result is true. - /// If the task completes because the handle is not signaled prior to the timeout, the task's result is false. - /// - /// - /// The completion of the returned task is asynchronous with respect to the code that actually signals the wait handle. - /// - public static Task ToTask(this WaitHandle handle, int timeout = Timeout.Infinite, CancellationToken cancellationToken = default(CancellationToken)) + public void GetResult() { - Requires.NotNull(handle, nameof(handle)); - - // Check whether the handle is already signaled as an optimization. - // But even for WaitOne(0) the CLR can pump messages if called on the UI thread, which the caller may not - // be expecting at this time, so be sure there is no message pump active by controlling the SynchronizationContext. - using (NoMessagePumpSyncContext.Default.Apply()) - { - if (handle.WaitOne(0)) - { - return TrueTask; - } - else if (timeout == 0) - { - return FalseTask; - } - } - - cancellationToken.ThrowIfCancellationRequested(); - var tcs = new TaskCompletionSource(); + // No need to do anything with 'task' because we already called Preserve on it. + } + } - RegisteredWaitHandle callbackHandle = ThreadPool.RegisterWaitForSingleObject( - handle, - static (state, timedOut) => ((TaskCompletionSource)state!).TrySetResult(!timedOut), - state: tcs, - millisecondsTimeOutInterval: timeout, - executeOnlyOnce: true); + /// + /// An awaitable that wraps a and never throws an exception when waited on. + /// + /// The type of the result. + public readonly struct NoThrowValueTaskAwaitable + { + /// + /// The task. + /// + private readonly ValueTask task; - if (cancellationToken.CanBeCanceled) - { - // Arrange that if the caller signals their cancellation token that we complete the task - // we return immediately. Because of the continuation we've scheduled on that task, this - // will automatically release the wait handle notification as well. - CancellationTokenRegistration cancellationRegistration = - cancellationToken.Register( - static state => - { - var tuple = (Tuple, CancellationToken>)state!; - tuple.Item1.TrySetCanceled(tuple.Item2); - }, - Tuple.Create(tcs, cancellationToken)); - - // We have a cancellation token registration and a wait handle registration to release. - // Each time this code executes, allocate one tuple as a state object to reduce from allocating an implicit closure *and* a delegate. - tcs.Task.ContinueWith( - static (_, state) => - { - var tuple = (Tuple)state!; - tuple.Item1.Unregister(null); // release resources for the async callback - tuple.Item2.Dispose(); // release memory for cancellation token registration - }, - Tuple.Create(callbackHandle, cancellationRegistration), - CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default); - } - else - { - // Since the cancellation token was the default one, the only thing we need to track is clearing the RegisteredWaitHandle, - // so do this such that we allocate as few objects as possible. - tcs.Task.ContinueWith( - static (_, state) => ((RegisteredWaitHandle)state!).Unregister(null), - callbackHandle, - CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default); - } + /// + /// A value indicating whether the continuation should be scheduled on the current sync context. + /// + private readonly bool captureContext; - return tcs.Task; + /// + /// Initializes a new instance of the struct. + /// + /// The task. + /// Whether the continuation should be scheduled on the current sync context. + public NoThrowValueTaskAwaitable(ValueTask task, bool captureContext) + { + this.task = task.Preserve(); + this.captureContext = captureContext; } /// - /// Applies one task's results to another. + /// Gets the awaiter. /// - /// The type of value returned by a task. - /// The task whose completion should be applied to another. - /// The task that should receive the completion status. - /// - /// true to complete the supplied as efficiently as possible (inline with the completion of ); - /// false to complete the asynchronously. - /// Note if is completed when this method is invoked, then is always completed synchronously. - /// - internal static void ApplyResultTo(this Task task, TaskCompletionSource tcs, bool inlineSubsequentCompletion) + /// The awaiter. + public NoThrowValueTaskAwaiter GetAwaiter() { - Requires.NotNull(task, nameof(task)); - Requires.NotNull(tcs, nameof(tcs)); - - if (task.IsCompleted) - { - ApplyCompletedTaskResultTo(task, tcs); - } - else - { - // Using a minimum of allocations (just one task, and no closure) ensure that one task's completion sets equivalent completion on another task. - task.ContinueWith( - (t, s) => ApplyCompletedTaskResultTo(t, (TaskCompletionSource)s!), - tcs, - CancellationToken.None, - inlineSubsequentCompletion ? TaskContinuationOptions.ExecuteSynchronously : TaskContinuationOptions.None, - TaskScheduler.Default); - } + return new NoThrowValueTaskAwaiter(this.task, this.captureContext); } + } + + /// + /// An awaiter that wraps a task and never throws an exception when waited on. + /// + /// The type of the result. + public readonly struct NoThrowValueTaskAwaiter : ICriticalNotifyCompletion + { + /// + /// The task. + /// + private readonly ValueTask task; /// - /// Returns a reusable task that is already canceled. + /// A value indicating whether the continuation should be scheduled on the current sync context. /// - /// The type parameter for the returned task. - internal static Task CanceledTaskOfT() => CanceledTaskOfTCache.CanceledTask; + private readonly bool captureContext; /// - /// Returns a that has been faulted with the specified exception. + /// Initializes a new instance of the struct. /// - /// The type of value that might have been returned from the . - /// The exception used to fault the . - /// The faulted task. - internal static Task FaultedTask(Exception ex) + /// The task. + /// if set to [capture context]. + public NoThrowValueTaskAwaiter(ValueTask task, bool captureContext) { - return Task.FromException(ex); + this.task = task; + this.captureContext = captureContext; } /// - /// Applies a completed task's results to another. + /// Gets a value indicating whether the task has completed. /// - /// The type of value returned by a task. - /// The task whose completion should be applied to another. - /// The task that should receive the completion status. - private static void ApplyCompletedTaskResultTo(Task completedTask, TaskCompletionSource taskCompletionSource) + public bool IsCompleted { - Assumes.NotNull(completedTask); - Assumes.True(completedTask.IsCompleted); - Assumes.NotNull(taskCompletionSource); - - if (completedTask.IsCanceled) - { - // NOTE: this is "lossy" in that we don't propagate any CancellationToken that the Task would throw an OperationCanceledException with. - // Propagating that data would require that we actually cause the completedTask to throw so we can inspect the - // OperationCanceledException.CancellationToken property, which we consider more costly than it's worth. - taskCompletionSource.TrySetCanceled(); - } - else if (completedTask.IsFaulted) - { - taskCompletionSource.TrySetException(completedTask.Exception!.InnerExceptions); - } - else - { - taskCompletionSource.TrySetResult(completedTask.Result); - } + get { return this.task.IsCompleted; } } /// - /// Applies a completed task's results to another. + /// Schedules a delegate for execution at the conclusion of a task's execution. /// - /// The type of value returned by a task. - /// The task whose completion should be applied to another. - /// The task that should receive the completion status. - /// The value to set on the completion source when the source task runs to completion. - private static void ApplyCompletedTaskResultTo(Task completedTask, TaskCompletionSource taskCompletionSource, T valueOnRanToCompletion) + /// The action. + public void OnCompleted(Action continuation) { - Assumes.NotNull(completedTask); - Assumes.True(completedTask.IsCompleted); - Assumes.NotNull(taskCompletionSource); - - if (completedTask.IsCanceled) - { - // NOTE: this is "lossy" in that we don't propagate any CancellationToken that the Task would throw an OperationCanceledException with. - // Propagating that data would require that we actually cause the completedTask to throw so we can inspect the - // OperationCanceledException.CancellationToken property, which we consider more costly than it's worth. - taskCompletionSource.TrySetCanceled(); - } - else if (completedTask.IsFaulted) - { - taskCompletionSource.TrySetException(completedTask.Exception!.InnerExceptions); - } - else - { - taskCompletionSource.TrySetResult(valueOnRanToCompletion); - } + this.task.ConfigureAwait(this.captureContext).GetAwaiter().OnCompleted(continuation); } /// - /// Gets a task that will eventually produce the result of another task, when that task finishes. - /// If that task is instead canceled, its successor will be followed for its result, iteratively. + /// Schedules a delegate for execution at the conclusion of a task's execution + /// without capturing the ExecutionContext. /// - /// The type of value returned by the task. - /// The TaskCompletionSource whose task is to follow. - /// The current task. - /// - /// The following task. - /// - private static Task FollowCancelableTaskToCompletionHelper(TaskCompletionSource, T> tcs, Task currentTask) + /// The action. + public void UnsafeOnCompleted(Action continuation) { - Requires.NotNull(tcs, nameof(tcs)); - Requires.NotNull(currentTask, nameof(currentTask)); - - currentTask.ContinueWith( - (t, state) => - { - var tcsNested = (TaskCompletionSource, T>)state!; - switch (t.Status) - { - case TaskStatus.RanToCompletion: - tcsNested.TrySetResult(t.Result); - tcsNested.SourceState.RegisteredCallback.Dispose(); - break; - case TaskStatus.Faulted: - tcsNested.TrySetException(t.Exception!.InnerExceptions); - tcsNested.SourceState.RegisteredCallback.Dispose(); - break; - case TaskStatus.Canceled: - Task? newTask = tcsNested.SourceState.CurrentTask; - Assumes.True(newTask != t, "A canceled task was not replaced with a new task."); - FollowCancelableTaskToCompletionHelper(tcsNested, newTask); - break; - } - }, - tcs, - tcs.SourceState.UltimateCancellation, - TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default); - - return tcs.Task; + this.task.ConfigureAwait(this.captureContext).GetAwaiter().UnsafeOnCompleted(continuation); } /// - /// An awaitable that wraps a task and never throws an exception when waited on. + /// Does nothing. /// - public readonly struct NoThrowTaskAwaitable + public void GetResult() { - /// - /// The task. - /// - private readonly Task task; - - /// - /// A value indicating whether the continuation should be scheduled on the current sync context. - /// - private readonly bool captureContext; + // No need to do anything with 'task' because we already called Preserve on it. + } + } - /// - /// Initializes a new instance of the struct. - /// - /// The task. - /// Whether the continuation should be scheduled on the current sync context. - public NoThrowTaskAwaitable(Task task, bool captureContext) - { - Requires.NotNull(task, nameof(task)); - this.task = task; - this.captureContext = captureContext; - } + /// + /// A state bag for the method. + /// + /// The type of value ultimately returned. + private readonly struct FollowCancelableTaskState + { + /// + /// The delegate that returns the task to follow. + /// + private readonly Func> getTaskToFollow; - /// - /// Gets the awaiter. - /// - /// The awaiter. - public NoThrowTaskAwaiter GetAwaiter() - { - return new NoThrowTaskAwaiter(this.task, this.captureContext); - } + /// + /// Initializes a new instance of the struct. + /// + /// The get task to follow. + /// The cancellation token. + internal FollowCancelableTaskState(Func> getTaskToFollow, CancellationToken cancellationToken) + : this(getTaskToFollow, registeredCallback: default, cancellationToken) + { } /// - /// An awaiter that wraps a task and never throws an exception when waited on. + /// Initializes a new instance of the struct. /// - public readonly struct NoThrowTaskAwaiter : ICriticalNotifyCompletion + /// The get task to follow. + /// The cancellation token registration to dispose of when the task completes normally. + /// The cancellation token. + private FollowCancelableTaskState(Func> getTaskToFollow, CancellationTokenRegistration registeredCallback, CancellationToken cancellationToken) { - /// - /// The task. - /// - private readonly Task task; - - /// - /// A value indicating whether the continuation should be scheduled on the current sync context. - /// - private readonly bool captureContext; + Requires.NotNull(getTaskToFollow, nameof(getTaskToFollow)); - /// - /// Initializes a new instance of the struct. - /// - /// The task. - /// if set to true [capture context]. - public NoThrowTaskAwaiter(Task task, bool captureContext) - { - Requires.NotNull(task, nameof(task)); - this.task = task; - this.captureContext = captureContext; - } - - /// - /// Gets a value indicating whether the task has completed. - /// - public bool IsCompleted - { - get { return this.task.IsCompleted; } - } - - /// - /// Schedules a delegate for execution at the conclusion of a task's execution. - /// - /// The action. - public void OnCompleted(Action continuation) - { - this.task.ConfigureAwait(this.captureContext).GetAwaiter().OnCompleted(continuation); - } - - /// - /// Schedules a delegate for execution at the conclusion of a task's execution - /// without capturing the ExecutionContext. - /// - /// The action. - public void UnsafeOnCompleted(Action continuation) - { - this.task.ConfigureAwait(this.captureContext).GetAwaiter().UnsafeOnCompleted(continuation); - } - - /// - /// Does nothing. - /// - public void GetResult() - { - // Never throw here. - } + this.getTaskToFollow = getTaskToFollow; + this.RegisteredCallback = registeredCallback; + this.UltimateCancellation = cancellationToken; } /// - /// A state bag for the method. + /// Gets the ultimate cancellation token. /// - /// The type of value ultimately returned. - private readonly struct FollowCancelableTaskState - { - /// - /// The delegate that returns the task to follow. - /// - private readonly Func> getTaskToFollow; + internal CancellationToken UltimateCancellation { get; } - /// - /// Initializes a new instance of the struct. - /// - /// The get task to follow. - /// The cancellation token. - internal FollowCancelableTaskState(Func> getTaskToFollow, CancellationToken cancellationToken) - : this(getTaskToFollow, registeredCallback: default, cancellationToken) - { - } + /// + /// Gets the cancellation token registration to dispose of when the task completes normally. + /// + internal CancellationTokenRegistration RegisteredCallback { get; } - /// - /// Initializes a new instance of the struct. - /// - /// The get task to follow. - /// The cancellation token registration to dispose of when the task completes normally. - /// The cancellation token. - private FollowCancelableTaskState(Func> getTaskToFollow, CancellationTokenRegistration registeredCallback, CancellationToken cancellationToken) + /// + /// Gets the current task to follow. + /// + internal Task CurrentTask + { + get { - Requires.NotNull(getTaskToFollow, nameof(getTaskToFollow)); - - this.getTaskToFollow = getTaskToFollow; - this.RegisteredCallback = registeredCallback; - this.UltimateCancellation = cancellationToken; + Task? task = this.getTaskToFollow(); + Assumes.NotNull(task); + return task; } + } - /// - /// Gets the ultimate cancellation token. - /// - internal CancellationToken UltimateCancellation { get; } - - /// - /// Gets the cancellation token registration to dispose of when the task completes normally. - /// - internal CancellationTokenRegistration RegisteredCallback { get; } - - /// - /// Gets the current task to follow. - /// - internal Task CurrentTask - { - get - { - Task? task = this.getTaskToFollow(); - Assumes.NotNull(task); - return task; - } - } + internal FollowCancelableTaskState WithRegisteredCallback(CancellationTokenRegistration registeredCallback) + => new FollowCancelableTaskState(this.getTaskToFollow, registeredCallback, this.UltimateCancellation); + } - internal FollowCancelableTaskState WithRegisteredCallback(CancellationTokenRegistration registeredCallback) - => new FollowCancelableTaskState(this.getTaskToFollow, registeredCallback, this.UltimateCancellation); - } + /// + /// A cache for canceled instances. + /// + /// The type parameter for the returned task. + private static class CanceledTaskOfTCache + { + /// + /// A task that is already canceled. + /// + internal static readonly Task CanceledTask = Task.FromCanceled(new CancellationToken(canceled: true)); + } + /// + /// A task completion source that contains additional state. + /// + /// The type of the state. + /// The type of the result. + private class TaskCompletionSource : TaskCompletionSource + { /// - /// A cache for canceled instances. + /// Initializes a new instance of the class. /// - /// The type parameter for the returned task. - private static class CanceledTaskOfTCache + /// The state to store in the property. + /// State of the task. + /// The options. + internal TaskCompletionSource(TState sourceState, object? taskState = null, TaskCreationOptions options = TaskCreationOptions.None) + : base(taskState, options) { - /// - /// A task that is already canceled. - /// - internal static readonly Task CanceledTask = Task.FromCanceled(new CancellationToken(canceled: true)); + this.SourceState = sourceState; } /// - /// A task completion source that contains additional state. + /// Gets or sets the state passed into the constructor. /// - /// The type of the state. - /// The type of the result. - private class TaskCompletionSource : TaskCompletionSource - { - /// - /// Initializes a new instance of the class. - /// - /// The state to store in the property. - /// State of the task. - /// The options. - internal TaskCompletionSource(TState sourceState, object? taskState = null, TaskCreationOptions options = TaskCreationOptions.None) - : base(taskState, options) - { - this.SourceState = sourceState; - } - - /// - /// Gets or sets the state passed into the constructor. - /// - internal TState SourceState { get; set; } - } + internal TState SourceState { get; set; } } } diff --git a/src/Microsoft.VisualStudio.Threading/WeakKeyDictionary`2.cs b/src/Microsoft.VisualStudio.Threading/WeakKeyDictionary`2.cs index e71f7b15a..5f92e4cc7 100644 --- a/src/Microsoft.VisualStudio.Threading/WeakKeyDictionary`2.cs +++ b/src/Microsoft.VisualStudio.Threading/WeakKeyDictionary`2.cs @@ -1,508 +1,507 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// Dictionary that does not prevent keys from being garbage collected. +/// +/// Type of key, without the WeakReference wrapper. +/// Type of value. +/// +/// See also Microsoft.Build.Collections.WeakDictionary. +/// +internal class WeakKeyDictionary : IEnumerable> + where TKey : class { - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Diagnostics.CodeAnalysis; + /// + /// The dictionary used internally to store the keys and values. + /// + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + private readonly Dictionary, TValue> dictionary; /// - /// Dictionary that does not prevent keys from being garbage collected. + /// The key comparer to use for hashing and equality checks. /// - /// Type of key, without the WeakReference wrapper. - /// Type of value. - /// - /// See also Microsoft.Build.Collections.WeakDictionary. - /// - internal class WeakKeyDictionary : IEnumerable> - where TKey : class + private readonly IEqualityComparer keyComparer; + + /// + /// The dictionary's initial capacity, and the capacity beyond which we will resist to grow + /// by scavenging for collected keys first. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private int capacity; + + /// + /// Initializes a new instance of the class. + /// + /// The key comparer to use. A value indicates the default comparer will be used. + /// The initial capacity of the dictionary. Growth beyond this capacity will first induce a scavenge operation. + public WeakKeyDictionary(IEqualityComparer? keyComparer = null, int capacity = 10) { - /// - /// The dictionary used internally to store the keys and values. - /// - [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - private readonly Dictionary, TValue> dictionary; + Requires.Range(capacity > 0, "capacity"); - /// - /// The key comparer to use for hashing and equality checks. - /// - private readonly IEqualityComparer keyComparer; + this.keyComparer = keyComparer ?? EqualityComparer.Default; + this.capacity = capacity; + IEqualityComparer> equalityComparer = new WeakReferenceEqualityComparer(this.keyComparer); + this.dictionary = new Dictionary, TValue>(this.capacity, equalityComparer); + } - /// - /// The dictionary's initial capacity, and the capacity beyond which we will resist to grow - /// by scavenging for collected keys first. - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private int capacity; + /// + /// Gets the number of entries in this dictionary. + /// Some entries may represent keys or values that have already been garbage collected. + /// To clean these out call . + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public int Count + { + get { return this.dictionary.Count; } + } - /// - /// Initializes a new instance of the class. - /// - /// The key comparer to use. A null value indicates the default comparer will be used. - /// The initial capacity of the dictionary. Growth beyond this capacity will first induce a scavenge operation. - public WeakKeyDictionary(IEqualityComparer? keyComparer = null, int capacity = 10) + /// + /// Gets all key values in the dictionary. + /// + internal IEnumerable Keys + { + get { - Requires.Range(capacity > 0, "capacity"); - - this.keyComparer = keyComparer ?? EqualityComparer.Default; - this.capacity = capacity; - IEqualityComparer> equalityComparer = new WeakReferenceEqualityComparer(this.keyComparer); - this.dictionary = new Dictionary, TValue>(this.capacity, equalityComparer); + return new KeyEnumerable(this); } + } - /// - /// Gets the number of entries in this dictionary. - /// Some entries may represent keys or values that have already been garbage collected. - /// To clean these out call . - /// - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - public int Count + /// + /// Obtains the value for a given key. + /// + public TValue this[TKey key] + { + get { - get { return this.dictionary.Count; } + WeakReference wrappedKey = new WeakReference(key, this.keyComparer, avoidWeakReferenceAllocation: true); + TValue value = this.dictionary[wrappedKey]; + return value; } - /// - /// Gets all key values in the dictionary. - /// - internal IEnumerable Keys + set { - get + WeakReference wrappedKey = new WeakReference(key, this.keyComparer); + + // Make some attempt to prevent dictionary growing forever with + // entries whose underlying key or value has already been collected. + // We do not have access to the dictionary's true capacity or growth + // method, so we improvise with our own. + // So attempt to make room for the upcoming add before we do it. + if (this.dictionary.Count == this.capacity && !this.ContainsKey(key)) { - return new KeyEnumerable(this); + this.Scavenge(); + + // If that didn't do anything, raise the capacity at which + // we next scavenge. Note that we never shrink, but neither + // does the underlying dictionary. + if (this.dictionary.Count == this.capacity) + { + this.capacity = this.dictionary.Count * 2; + } } + + this.dictionary[wrappedKey] = value; } + } - /// - /// Obtains the value for a given key. - /// - public TValue this[TKey key] + /// + /// Whether there is a key present with the specified key. + /// + /// + /// As usual, don't just call Contained as the wrapped value may be null. + /// + public bool ContainsKey(TKey key) + { +#pragma warning disable CS8717 // A member returning a [MaybeNull] value introduces a null value for a type parameter. + bool contained = this.TryGetValue(key, out TValue? value); +#pragma warning restore CS8717 // A member returning a [MaybeNull] value introduces a null value for a type parameter. + return contained; + } + + /// + /// Attempts to get the value for the provided key. + /// Returns true if the key is found, otherwise false. + /// + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { +#pragma warning disable CS8717 // A member returning a [MaybeNull] value introduces a null value for a type parameter. https://github.com/dotnet/roslyn/issues/39656 + return this.dictionary.TryGetValue(new WeakReference(key, this.keyComparer, avoidWeakReferenceAllocation: true), out value); +#pragma warning restore CS8717 // A member returning a [MaybeNull] value introduces a null value for a type parameter. + } + + /// + /// Removes an entry with the specified key. + /// Returns true if found, false otherwise. + /// + public bool Remove(TKey key) + { + return this.dictionary.Remove(new WeakReference(key, this.keyComparer, avoidWeakReferenceAllocation: true)); + } + + /// + /// Remove any entries from the dictionary that represent keys + /// that have been garbage collected. + /// + /// The number of entries removed. + public int Scavenge() + { + List>? remove = null; + + foreach (WeakReference weakKey in this.dictionary.Keys) { - get + if (!weakKey.IsAlive) { - WeakReference wrappedKey = new WeakReference(key, this.keyComparer, avoidWeakReferenceAllocation: true); - TValue value = this.dictionary[wrappedKey]; - return value; + remove = remove ?? new List>(); + remove.Add(weakKey); } + } - set + if (remove is object) + { + foreach (WeakReference entry in remove) { - WeakReference wrappedKey = new WeakReference(key, this.keyComparer); - - // Make some attempt to prevent dictionary growing forever with - // entries whose underlying key or value has already been collected. - // We do not have access to the dictionary's true capacity or growth - // method, so we improvise with our own. - // So attempt to make room for the upcoming add before we do it. - if (this.dictionary.Count == this.capacity && !this.ContainsKey(key)) - { - this.Scavenge(); - - // If that didn't do anything, raise the capacity at which - // we next scavenge. Note that we never shrink, but neither - // does the underlying dictionary. - if (this.dictionary.Count == this.capacity) - { - this.capacity = this.dictionary.Count * 2; - } - } + this.dictionary.Remove(entry); + } + + return remove.Count; + } - this.dictionary[wrappedKey] = value; + return 0; + } + + /// + /// Empty the collection. + /// + public void Clear() + { + this.dictionary.Clear(); + } + + /// + /// See IEnumerable<T>. + /// + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + /// + /// See IEnumerable<T>. + /// + IEnumerator> IEnumerable>.GetEnumerator() + { + return this.GetEnumerator(); + } + + /// + /// See IEnumerable. + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + /// + /// Whether the collection contains any item. + /// + internal bool Any() + { + foreach (KeyValuePair, TValue> item in this.dictionary) + { + if (item.Key.IsAlive) + { + return true; } } - /// - /// Whether there is a key present with the specified key. - /// - /// - /// As usual, don't just call Contained as the wrapped value may be null. - /// - public bool ContainsKey(TKey key) + return false; + } + + public struct Enumerator : IEnumerator> + { + private Dictionary, TValue>.Enumerator enumerator; + + private KeyValuePair current; + + internal Enumerator(WeakKeyDictionary dictionary) { -#pragma warning disable CS8717 // A member returning a [MaybeNull] value introduces a null value for a type parameter. - bool contained = this.TryGetValue(key, out TValue? value); -#pragma warning restore CS8717 // A member returning a [MaybeNull] value introduces a null value for a type parameter. - return contained; + Requires.NotNull(dictionary, nameof(dictionary)); + + this.enumerator = dictionary.dictionary.GetEnumerator(); + this.current = default(KeyValuePair); } - /// - /// Attempts to get the value for the provided key. - /// Returns true if the key is found, otherwise false. - /// - public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + public KeyValuePair Current { -#pragma warning disable CS8717 // A member returning a [MaybeNull] value introduces a null value for a type parameter. https://github.com/dotnet/roslyn/issues/39656 - return this.dictionary.TryGetValue(new WeakReference(key, this.keyComparer, avoidWeakReferenceAllocation: true), out value); -#pragma warning restore CS8717 // A member returning a [MaybeNull] value introduces a null value for a type parameter. + get { return this.current; } } - /// - /// Removes an entry with the specified key. - /// Returns true if found, false otherwise. - /// - public bool Remove(TKey key) + object System.Collections.IEnumerator.Current { - return this.dictionary.Remove(new WeakReference(key, this.keyComparer, avoidWeakReferenceAllocation: true)); + get { return this.Current; } } - /// - /// Remove any entries from the dictionary that represent keys - /// that have been garbage collected. - /// - /// The number of entries removed. - public int Scavenge() + public bool MoveNext() { - List>? remove = null; + TKey? key = null; - foreach (WeakReference weakKey in this.dictionary.Keys) + while (this.enumerator.MoveNext()) { - if (!weakKey.IsAlive) + key = this.enumerator.Current.Key.Target; + if (key is object) { - remove = remove ?? new List>(); - remove.Add(weakKey); + this.current = new KeyValuePair(key, this.enumerator.Current.Value); + return true; } } - if (remove is object) - { - foreach (WeakReference entry in remove) - { - this.dictionary.Remove(entry); - } + return false; + } - return remove.Count; - } + void System.Collections.IEnumerator.Reset() + { + // Calling reset on the dictionary enumerator would require boxing it in the cast to the explicit interface method. + // But boxing a valuetype means that any changes you make will not be brought back to the value type field + // so the Reset() will probably have no effect. + // If we ever have to support this, we'll probably have to do box the enumerator and then retain the boxed + // version and use that in this enumerator for the rest of its lifetime. + throw new NotSupportedException(); + } - return 0; + public void Dispose() + { + this.enumerator.Dispose(); } + } + /// + /// Strongly typed wrapper around a weak reference that caches + /// the target's hash code so that it can be used in a hashtable. + /// + /// Type of the target of the weak reference. + private readonly struct WeakReference : IEquatable> + where T : class + { /// - /// Empty the collection. + /// Cache the hashcode so that it is still available even if the target has been + /// collected. This allows this object to be still found in a table so it can be removed. /// - public void Clear() - { - this.dictionary.Clear(); - } + private readonly int hashcode; /// - /// See IEnumerable<T>. + /// Backing weak reference. /// - public Enumerator GetEnumerator() + private readonly WeakReference? weakReference; + + /// + /// Some of the instances are around just to do existence checks, and don't want + /// to allocate WeakReference objects as they are short-lived. + /// + private readonly T? notSoWeakTarget; + + /// + /// Initializes a new instance of the struct. + /// + internal WeakReference(T target, IEqualityComparer equalityComparer, bool avoidWeakReferenceAllocation = false) { - return new Enumerator(this); + Requires.NotNull(target, nameof(target)); + Requires.NotNull(equalityComparer, nameof(equalityComparer)); + + this.notSoWeakTarget = avoidWeakReferenceAllocation ? target : null; + this.weakReference = avoidWeakReferenceAllocation ? null : new WeakReference(target); + this.hashcode = equalityComparer.GetHashCode(target); } /// - /// See IEnumerable<T>. + /// Gets the target wrapped by this weak reference. Null if the target has already been garbage collected. /// - IEnumerator> IEnumerable>.GetEnumerator() + internal T? Target { - return this.GetEnumerator(); + get { return this.notSoWeakTarget ?? (T?)this.weakReference?.Target; } } /// - /// See IEnumerable. + /// Gets a value indicating whether the target has not been garbage collected yet. /// - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + internal bool IsAlive { - return this.GetEnumerator(); + get { return this.notSoWeakTarget is object || (this.weakReference?.IsAlive ?? false); } } /// - /// Whether the collection contains any item. + /// Returns the hashcode of the wrapped target. /// - internal bool Any() + public override int GetHashCode() { - foreach (KeyValuePair.WeakReference, TValue> item in this.dictionary) - { - if (item.Key.IsAlive) - { - return true; - } - } - - return false; + return this.hashcode; } - public struct Enumerator : IEnumerator> + /// + /// Compares two structures. + /// + public override bool Equals(object? obj) { - private Dictionary, TValue>.Enumerator enumerator; - - private KeyValuePair current; - - internal Enumerator(WeakKeyDictionary dictionary) - { - Requires.NotNull(dictionary, nameof(dictionary)); - - this.enumerator = dictionary.dictionary.GetEnumerator(); - this.current = default(KeyValuePair); - } + // We can't implement equals in the same terms as GetHashCode() because + // our target object may have been collected. Instead just go based on + // equality of our weak references. + return obj is WeakReference other && this.Equals(other); + } - public KeyValuePair Current - { - get { return this.current; } - } + /// + public bool Equals(WeakReference other) => Equals(this.weakReference, other.weakReference); + } - object System.Collections.IEnumerator.Current - { - get { return this.Current; } - } + /// + /// A helper structure to implement . + /// + private class KeyEnumerator : IEnumerator + { + private Dictionary, TValue>.Enumerator enumerator; - public bool MoveNext() - { - TKey? key = null; + internal KeyEnumerator(WeakKeyDictionary dictionary) + { + Requires.NotNull(dictionary, nameof(dictionary)); - while (this.enumerator.MoveNext()) - { - key = this.enumerator.Current.Key.Target; - if (key is object) - { - this.current = new KeyValuePair(key, this.enumerator.Current.Value); - return true; - } - } + // Assign a value to Current to suppress CS8618. The Current property may have a null value at times, + // but the value will never be exposed to external code provided the code only accesses Current after a + // call to MoveNext returns true. + this.Current = null!; - return false; - } + this.enumerator = dictionary.dictionary.GetEnumerator(); + } - void System.Collections.IEnumerator.Reset() - { - // Calling reset on the dictionary enumerator would require boxing it in the cast to the explicit interface method. - // But boxing a valuetype means that any changes you make will not be brought back to the value type field - // so the Reset() will probably have no effect. - // If we ever have to support this, we'll probably have to do box the enumerator and then retain the boxed - // version and use that in this enumerator for the rest of its lifetime. - throw new NotSupportedException(); - } + /// + /// Gets the current item of the enumerator. + /// + public TKey Current { get; private set; } - public void Dispose() - { - this.enumerator.Dispose(); - } - } + object System.Collections.IEnumerator.Current => this.Current; /// - /// Strongly typed wrapper around a weak reference that caches - /// the target's hash code so that it can be used in a hashtable. + /// Implements . /// - /// Type of the target of the weak reference. - private readonly struct WeakReference : IEquatable> - where T : class + public bool MoveNext() { - /// - /// Cache the hashcode so that it is still available even if the target has been - /// collected. This allows this object to be still found in a table so it can be removed. - /// - private readonly int hashcode; - - /// - /// Backing weak reference. - /// - private readonly WeakReference? weakReference; - - /// - /// Some of the instances are around just to do existence checks, and don't want - /// to allocate WeakReference objects as they are short-lived. - /// - private readonly T? notSoWeakTarget; - - /// - /// Initializes a new instance of the struct. - /// - internal WeakReference(T target, IEqualityComparer equalityComparer, bool avoidWeakReferenceAllocation = false) + while (this.enumerator.MoveNext()) { - Requires.NotNull(target, nameof(target)); - Requires.NotNull(equalityComparer, nameof(equalityComparer)); - - this.notSoWeakTarget = avoidWeakReferenceAllocation ? target : null; - this.weakReference = avoidWeakReferenceAllocation ? null : new WeakReference(target); - this.hashcode = equalityComparer.GetHashCode(target); + TKey? key = this.enumerator.Current.Key.Target; + if (key is object) + { + this.Current = key; + return true; + } } - /// - /// Gets the target wrapped by this weak reference. Null if the target has already been garbage collected. - /// - internal T? Target - { - get { return this.notSoWeakTarget ?? (T?)this.weakReference?.Target; } - } + return false; + } - /// - /// Gets a value indicating whether the target has not been garbage collected yet. - /// - internal bool IsAlive - { - get { return this.notSoWeakTarget is object || (this.weakReference?.IsAlive ?? false); } - } + void System.Collections.IEnumerator.Reset() + { + // Calling reset on the dictionary enumerator would require boxing it in the cast to the explicit interface method. + // But boxing a valuetype means that any changes you make will not be brought back to the value type field + // so the Reset() will probably have no effect. + // If we ever have to support this, we'll probably have to do box the enumerator and then retain the boxed + // version and use that in this enumerator for the rest of its lifetime. + throw new NotSupportedException(); + } - /// - /// Returns the hashcode of the wrapped target. - /// - public override int GetHashCode() - { - return this.hashcode; - } + public void Dispose() + { + this.enumerator.Dispose(); + } + } - /// - /// Compares two structures. - /// - public override bool Equals(object? obj) - { - // We can't implement equals in the same terms as GetHashCode() because - // our target object may have been collected. Instead just go based on - // equality of our weak references. - return obj is WeakReference other && this.Equals(other); - } + /// + /// A helper structure to enumerate keys in the dictionary. + /// + private class KeyEnumerable : IEnumerable + { + private readonly WeakKeyDictionary dictionary; - /// - public bool Equals(WeakReference other) => Equals(this.weakReference, other.weakReference); + internal KeyEnumerable(WeakKeyDictionary dictionary) + { + Requires.NotNull(dictionary, nameof(dictionary)); + this.dictionary = dictionary; } /// - /// A helper structure to implement . + /// Implements . /// - private class KeyEnumerator : IEnumerator + IEnumerator IEnumerable.GetEnumerator() { - private Dictionary, TValue>.Enumerator enumerator; - - internal KeyEnumerator(WeakKeyDictionary dictionary) - { - Requires.NotNull(dictionary, nameof(dictionary)); - - // Assign a value to Current to suppress CS8618. The Current property may have a null value at times, - // but the value will never be exposed to external code provided the code only accesses Current after a - // call to MoveNext returns true. - this.Current = null!; - - this.enumerator = dictionary.dictionary.GetEnumerator(); - } - - /// - /// Gets the current item of the enumerator. - /// - public TKey Current { get; private set; } - - object System.Collections.IEnumerator.Current => this.Current; - - /// - /// Implements . - /// - public bool MoveNext() - { - while (this.enumerator.MoveNext()) - { - TKey? key = this.enumerator.Current.Key.Target; - if (key is object) - { - this.Current = key; - return true; - } - } - - return false; - } - - void System.Collections.IEnumerator.Reset() - { - // Calling reset on the dictionary enumerator would require boxing it in the cast to the explicit interface method. - // But boxing a valuetype means that any changes you make will not be brought back to the value type field - // so the Reset() will probably have no effect. - // If we ever have to support this, we'll probably have to do box the enumerator and then retain the boxed - // version and use that in this enumerator for the rest of its lifetime. - throw new NotSupportedException(); - } - - public void Dispose() - { - this.enumerator.Dispose(); - } + return this.GetEnumerator(); } /// - /// A helper structure to enumerate keys in the dictionary. + /// Implements . /// - private class KeyEnumerable : IEnumerable + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { - private readonly WeakKeyDictionary dictionary; + return this.GetEnumerator(); + } - internal KeyEnumerable(WeakKeyDictionary dictionary) - { - Requires.NotNull(dictionary, nameof(dictionary)); - this.dictionary = dictionary; - } + /// + /// Gets the Enumerator. + /// + /// A new KeyEnumerator. + private KeyEnumerator GetEnumerator() + { + return new KeyEnumerator(this.dictionary); + } + } - /// - /// Implements . - /// - IEnumerator IEnumerable.GetEnumerator() - { - return this.GetEnumerator(); - } + /// + /// Equality comparer for weak references that actually compares the + /// targets of the weak references. + /// + /// Type of the targets of the weak references to be compared. + private class WeakReferenceEqualityComparer : IEqualityComparer> + where T : class + { + /// + /// Comparer to use if specified, otherwise null. + /// + private readonly IEqualityComparer underlyingComparer; - /// - /// Implements . - /// - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() - { - return this.GetEnumerator(); - } + /// + /// Initializes a new instance of the class + /// with an explicitly specified comparer. + /// + /// + /// May be null, in which case the default comparer for the type will be used. + /// + internal WeakReferenceEqualityComparer(IEqualityComparer comparer) + { + Requires.NotNull(comparer, nameof(comparer)); - /// - /// Gets the Enumerator. - /// - /// A new KeyEnumerator. - private KeyEnumerator GetEnumerator() - { - return new KeyEnumerator(this.dictionary); - } + this.underlyingComparer = comparer; } /// - /// Equality comparer for weak references that actually compares the - /// targets of the weak references. + /// Gets the hashcode. /// - /// Type of the targets of the weak references to be compared. - private class WeakReferenceEqualityComparer : IEqualityComparer> - where T : class + public int GetHashCode(WeakReference item) { - /// - /// Comparer to use if specified, otherwise null. - /// - private readonly IEqualityComparer underlyingComparer; - - /// - /// Initializes a new instance of the class - /// with an explicitly specified comparer. - /// - /// - /// May be null, in which case the default comparer for the type will be used. - /// - internal WeakReferenceEqualityComparer(IEqualityComparer comparer) - { - Requires.NotNull(comparer, nameof(comparer)); - - this.underlyingComparer = comparer; - } - - /// - /// Gets the hashcode. - /// - public int GetHashCode(WeakReference item) - { - // item.GetHashCode() returns a cached value from when the Target was referenced, - // and was calculated using this.underlyingComparer. - return item.GetHashCode(); - } + // item.GetHashCode() returns a cached value from when the Target was referenced, + // and was calculated using this.underlyingComparer. + return item.GetHashCode(); + } - /// - /// Compares the weak references for equality. - /// - public bool Equals(WeakReference left, WeakReference right) - { - // PERF: do not add any code here that will cause the value type parameters to be boxed! - return this.underlyingComparer.Equals(left.Target, right.Target); - } + /// + /// Compares the weak references for equality. + /// + public bool Equals(WeakReference left, WeakReference right) + { + // PERF: do not add any code here that will cause the value type parameters to be boxed! + return this.underlyingComparer.Equals(left.Target, right.Target); } } } diff --git a/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Shipped.txt b/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Shipped.txt deleted file mode 100644 index 56d837e4e..000000000 --- a/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Shipped.txt +++ /dev/null @@ -1,456 +0,0 @@ -#nullable enable -Microsoft.VisualStudio.Threading.AsyncAutoResetEvent -Microsoft.VisualStudio.Threading.AsyncAutoResetEvent.AsyncAutoResetEvent() -> void -Microsoft.VisualStudio.Threading.AsyncAutoResetEvent.AsyncAutoResetEvent(bool allowInliningAwaiters) -> void -Microsoft.VisualStudio.Threading.AsyncAutoResetEvent.Set() -> void -Microsoft.VisualStudio.Threading.AsyncAutoResetEvent.WaitAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncAutoResetEvent.WaitAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncBarrier -Microsoft.VisualStudio.Threading.AsyncBarrier.AsyncBarrier(int participants) -> void -Microsoft.VisualStudio.Threading.AsyncBarrier.SignalAndWait() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncCountdownEvent -Microsoft.VisualStudio.Threading.AsyncCountdownEvent.AsyncCountdownEvent(int initialCount) -> void -Microsoft.VisualStudio.Threading.AsyncCountdownEvent.Signal() -> void -Microsoft.VisualStudio.Threading.AsyncCountdownEvent.SignalAndWaitAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncCountdownEvent.SignalAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncCountdownEvent.WaitAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncEventHandler -Microsoft.VisualStudio.Threading.AsyncEventHandler -Microsoft.VisualStudio.Threading.AsyncLazy -Microsoft.VisualStudio.Threading.AsyncLazy.AsyncLazy(System.Func!>! valueFactory, Microsoft.VisualStudio.Threading.JoinableTaskFactory? joinableTaskFactory = null) -> void -Microsoft.VisualStudio.Threading.AsyncLazy.GetValue() -> T -Microsoft.VisualStudio.Threading.AsyncLazy.GetValue(System.Threading.CancellationToken cancellationToken) -> T -Microsoft.VisualStudio.Threading.AsyncLazy.GetValueAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncLazy.GetValueAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncLazy.IsValueCreated.get -> bool -Microsoft.VisualStudio.Threading.AsyncLazy.IsValueFactoryCompleted.get -> bool -Microsoft.VisualStudio.Threading.AsyncLazyInitializer -Microsoft.VisualStudio.Threading.AsyncLazyInitializer.AsyncLazyInitializer(System.Func! action, Microsoft.VisualStudio.Threading.JoinableTaskFactory? joinableTaskFactory = null) -> void -Microsoft.VisualStudio.Threading.AsyncLazyInitializer.Initialize(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void -Microsoft.VisualStudio.Threading.AsyncLazyInitializer.InitializeAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncLazyInitializer.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AsyncLazyInitializer.IsCompletedSuccessfully.get -> bool -Microsoft.VisualStudio.Threading.AsyncLocal -Microsoft.VisualStudio.Threading.AsyncLocal.AsyncLocal() -> void -Microsoft.VisualStudio.Threading.AsyncLocal.Value.get -> T? -Microsoft.VisualStudio.Threading.AsyncLocal.Value.set -> void -Microsoft.VisualStudio.Threading.AsyncManualResetEvent -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.AsyncManualResetEvent(bool initialState = false, bool allowInliningAwaiters = false) -> void -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.GetAwaiter() -> System.Runtime.CompilerServices.TaskAwaiter -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.IsSet.get -> bool -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.PulseAll() -> void -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.PulseAllAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.Reset() -> void -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.Set() -> void -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.SetAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.WaitAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.WaitAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncQueue -Microsoft.VisualStudio.Threading.AsyncQueue.AsyncQueue() -> void -Microsoft.VisualStudio.Threading.AsyncQueue.Complete() -> void -Microsoft.VisualStudio.Threading.AsyncQueue.Completion.get -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncQueue.Count.get -> int -Microsoft.VisualStudio.Threading.AsyncQueue.DequeueAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncQueue.Enqueue(T value) -> void -Microsoft.VisualStudio.Threading.AsyncQueue.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AsyncQueue.IsEmpty.get -> bool -Microsoft.VisualStudio.Threading.AsyncQueue.Peek() -> T -Microsoft.VisualStudio.Threading.AsyncQueue.SyncRoot.get -> object! -Microsoft.VisualStudio.Threading.AsyncQueue.TryDequeue(System.Predicate! valueCheck, out T value) -> bool -Microsoft.VisualStudio.Threading.AsyncQueue.TryDequeue(out T value) -> bool -Microsoft.VisualStudio.Threading.AsyncQueue.TryEnqueue(T value) -> bool -Microsoft.VisualStudio.Threading.AsyncQueue.TryPeek(out T value) -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.AmbientLock.get -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.AsyncReaderWriterLock() -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.AsyncReaderWriterLock(bool captureDiagnostics) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaiter! -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaiter -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaiter.GetResult() -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Releaser -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaiter.UnsafeOnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.CaptureDiagnostics.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.CaptureDiagnostics.set -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Complete() -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Completion.get -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Dispose() -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.GetAggregateLockFlags() -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.HideLocks() -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Suppression -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsAnyLockHeld.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsAnyPassiveLockHeld.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsPassiveReadLockHeld.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsPassiveUpgradeableReadLockHeld.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsPassiveWriteLockHeld.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsReadLockHeld.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsUpgradeableReadLockHeld.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsWriteLockHeld.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags.None = 0 -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags.StickyWrite = 1 -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.Data.get -> object? -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.Data.set -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.Flags.get -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.HasReadLock.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.HasUpgradeableReadLock.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.HasWriteLock.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.IsActive.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.IsReadLock.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.IsUpgradeableReadLock.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.IsValid.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.IsWriteLock.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.NestingLock.get -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockStackContains(Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags flags, Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle handle) -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.OnBeforeWriteLockReleased(System.Func! action) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.OnCriticalFailure(string! message) -> System.Exception! -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.ReadLockAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Releaser -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Releaser.Dispose() -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Releaser.ReleaseAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Suppression -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Suppression.Dispose() -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.SyncObject.get -> object! -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.UpgradeableReadLockAsync(Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.UpgradeableReadLockAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.WriteLockAsync(Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.WriteLockAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.AsyncReaderWriterResourceLock() -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.AsyncReaderWriterResourceLock(bool captureDiagnostics) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.GetAggregateLockFlags() -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags.None = 0 -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags.SkipInitialPreparation = 4096 -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags.StickyWrite = 1 -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ReadLockAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaiter -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaiter -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaiter.GetResult() -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceReleaser -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceReleaser -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceReleaser.Dispose() -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceReleaser.GetResourceAsync(TMoniker resourceMoniker, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceReleaser.ReleaseAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.SetAllResourcesToUnknownState() -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.SetResourceAsAccessed(System.Func! resourceCheck, object? state) -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.SetResourceAsAccessed(TResource! resource) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.UpgradeableReadLockAsync(Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.UpgradeableReadLockAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.WriteLockAsync(Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.WriteLockAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaitable -Microsoft.VisualStudio.Threading.AsyncSemaphore -Microsoft.VisualStudio.Threading.AsyncSemaphore.AsyncSemaphore(int initialCount) -> void -Microsoft.VisualStudio.Threading.AsyncSemaphore.CurrentCount.get -> int -Microsoft.VisualStudio.Threading.AsyncSemaphore.Dispose() -> void -Microsoft.VisualStudio.Threading.AsyncSemaphore.EnterAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncSemaphore.EnterAsync(System.TimeSpan timeout, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncSemaphore.EnterAsync(int timeout, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncSemaphore.Releaser -Microsoft.VisualStudio.Threading.AsyncSemaphore.Releaser.Dispose() -> void -Microsoft.VisualStudio.Threading.AwaitExtensions -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaitable -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaitable.ConfiguredTaskYieldAwaitable(bool continueOnCapturedContext) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaiter.ConfiguredTaskYieldAwaiter(bool continueOnCapturedContext) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaiter.GetResult() -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable.ExecuteContinuationSynchronouslyAwaitable(System.Threading.Tasks.Task! antecedent) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable.ExecuteContinuationSynchronouslyAwaitable(System.Threading.Tasks.Task! antecedent) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter.ExecuteContinuationSynchronouslyAwaiter(System.Threading.Tasks.Task! antecedent) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter.GetResult() -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter.ExecuteContinuationSynchronouslyAwaiter(System.Threading.Tasks.Task! antecedent) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter.GetResult() -> T -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaitable -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaitable.TaskSchedulerAwaitable(System.Threading.Tasks.TaskScheduler! taskScheduler, bool alwaysYield = false) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaiter.GetResult() -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaiter.TaskSchedulerAwaiter(System.Threading.Tasks.TaskScheduler! scheduler, bool alwaysYield = false) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.CancellationTokenExtensions -Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken -Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.CombinedCancellationToken(System.Threading.CancellationToken cancellationToken) -> void -Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.CombinedCancellationToken(System.Threading.CancellationTokenSource! cancellationTokenSource) -> void -Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.Dispose() -> void -Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.Equals(Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken other) -> bool -Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.Token.get -> System.Threading.CancellationToken -Microsoft.VisualStudio.Threading.DelegatingJoinableTaskFactory -Microsoft.VisualStudio.Threading.DelegatingJoinableTaskFactory.DelegatingJoinableTaskFactory(Microsoft.VisualStudio.Threading.JoinableTaskFactory! innerFactory) -> void -Microsoft.VisualStudio.Threading.DispatcherExtensions -Microsoft.VisualStudio.Threading.HangReportContribution -Microsoft.VisualStudio.Threading.HangReportContribution.Content.get -> string! -Microsoft.VisualStudio.Threading.HangReportContribution.ContentName.get -> string? -Microsoft.VisualStudio.Threading.HangReportContribution.ContentType.get -> string? -Microsoft.VisualStudio.Threading.HangReportContribution.HangReportContribution(string! content, string? contentType, string? contentName) -> void -Microsoft.VisualStudio.Threading.HangReportContribution.HangReportContribution(string! content, string? contentType, string? contentName, params Microsoft.VisualStudio.Threading.HangReportContribution![]? nestedReports) -> void -Microsoft.VisualStudio.Threading.HangReportContribution.NestedReports.get -> System.Collections.Generic.IReadOnlyCollection? -Microsoft.VisualStudio.Threading.IAsyncDisposable -Microsoft.VisualStudio.Threading.IAsyncDisposable.DisposeAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.IHangReportContributor -Microsoft.VisualStudio.Threading.IHangReportContributor.GetHangReport() -> Microsoft.VisualStudio.Threading.HangReportContribution! -Microsoft.VisualStudio.Threading.JoinableTask -Microsoft.VisualStudio.Threading.JoinableTask.GetAwaiter() -> System.Runtime.CompilerServices.TaskAwaiter -Microsoft.VisualStudio.Threading.JoinableTask.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.JoinableTask.Join(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void -Microsoft.VisualStudio.Threading.JoinableTask.JoinAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.JoinableTask.Task.get -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.JoinableTask -Microsoft.VisualStudio.Threading.JoinableTask.GetAwaiter() -> System.Runtime.CompilerServices.TaskAwaiter -Microsoft.VisualStudio.Threading.JoinableTask.Join(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> T -Microsoft.VisualStudio.Threading.JoinableTask.JoinAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.JoinableTask.Task.get -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.JoinableTaskCollection -Microsoft.VisualStudio.Threading.JoinableTaskCollection.Add(Microsoft.VisualStudio.Threading.JoinableTask! joinableTask) -> void -Microsoft.VisualStudio.Threading.JoinableTaskCollection.Contains(Microsoft.VisualStudio.Threading.JoinableTask! joinableTask) -> bool -Microsoft.VisualStudio.Threading.JoinableTaskCollection.Context.get -> Microsoft.VisualStudio.Threading.JoinableTaskContext! -Microsoft.VisualStudio.Threading.JoinableTaskCollection.DisplayName.get -> string? -Microsoft.VisualStudio.Threading.JoinableTaskCollection.DisplayName.set -> void -Microsoft.VisualStudio.Threading.JoinableTaskCollection.GetEnumerator() -> System.Collections.Generic.IEnumerator! -Microsoft.VisualStudio.Threading.JoinableTaskCollection.Join() -> Microsoft.VisualStudio.Threading.JoinableTaskCollection.JoinRelease -Microsoft.VisualStudio.Threading.JoinableTaskCollection.JoinRelease -Microsoft.VisualStudio.Threading.JoinableTaskCollection.JoinRelease.Dispose() -> void -Microsoft.VisualStudio.Threading.JoinableTaskCollection.JoinTillEmptyAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.JoinableTaskCollection.JoinTillEmptyAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.JoinableTaskCollection.JoinableTaskCollection(Microsoft.VisualStudio.Threading.JoinableTaskContext! context, bool refCountAddedJobs = false) -> void -Microsoft.VisualStudio.Threading.JoinableTaskCollection.Remove(Microsoft.VisualStudio.Threading.JoinableTask! joinableTask) -> void -Microsoft.VisualStudio.Threading.JoinableTaskContext -Microsoft.VisualStudio.Threading.JoinableTaskContext.CreateCollection() -> Microsoft.VisualStudio.Threading.JoinableTaskCollection! -Microsoft.VisualStudio.Threading.JoinableTaskContext.Dispose() -> void -Microsoft.VisualStudio.Threading.JoinableTaskContext.Factory.get -> Microsoft.VisualStudio.Threading.JoinableTaskFactory! -Microsoft.VisualStudio.Threading.JoinableTaskContext.HangDetails -Microsoft.VisualStudio.Threading.JoinableTaskContext.HangDetails.EntryMethod.get -> System.Reflection.MethodInfo? -Microsoft.VisualStudio.Threading.JoinableTaskContext.HangDetails.HangDetails(System.TimeSpan hangDuration, int notificationCount, System.Guid hangId, System.Reflection.MethodInfo? entryMethod) -> void -Microsoft.VisualStudio.Threading.JoinableTaskContext.HangDetails.HangDuration.get -> System.TimeSpan -Microsoft.VisualStudio.Threading.JoinableTaskContext.HangDetails.HangId.get -> System.Guid -Microsoft.VisualStudio.Threading.JoinableTaskContext.HangDetails.NotificationCount.get -> int -Microsoft.VisualStudio.Threading.JoinableTaskContext.IsMainThreadBlocked() -> bool -Microsoft.VisualStudio.Threading.JoinableTaskContext.IsOnMainThread.get -> bool -Microsoft.VisualStudio.Threading.JoinableTaskContext.IsWithinJoinableTask.get -> bool -Microsoft.VisualStudio.Threading.JoinableTaskContext.JoinableTaskContext() -> void -Microsoft.VisualStudio.Threading.JoinableTaskContext.JoinableTaskContext(System.Threading.Thread? mainThread = null, System.Threading.SynchronizationContext? synchronizationContext = null) -> void -Microsoft.VisualStudio.Threading.JoinableTaskContext.MainThread.get -> System.Threading.Thread! -Microsoft.VisualStudio.Threading.JoinableTaskContext.RevertRelevance -Microsoft.VisualStudio.Threading.JoinableTaskContext.RevertRelevance.Dispose() -> void -Microsoft.VisualStudio.Threading.JoinableTaskContext.SuppressRelevance() -> Microsoft.VisualStudio.Threading.JoinableTaskContext.RevertRelevance -Microsoft.VisualStudio.Threading.JoinableTaskContextException -Microsoft.VisualStudio.Threading.JoinableTaskContextException.JoinableTaskContextException() -> void -Microsoft.VisualStudio.Threading.JoinableTaskContextException.JoinableTaskContextException(System.Runtime.Serialization.SerializationInfo! info, System.Runtime.Serialization.StreamingContext context) -> void -Microsoft.VisualStudio.Threading.JoinableTaskContextException.JoinableTaskContextException(string? message) -> void -Microsoft.VisualStudio.Threading.JoinableTaskContextException.JoinableTaskContextException(string? message, System.Exception? inner) -> void -Microsoft.VisualStudio.Threading.JoinableTaskContextNode -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.Context.get -> Microsoft.VisualStudio.Threading.JoinableTaskContext! -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.CreateCollection() -> Microsoft.VisualStudio.Threading.JoinableTaskCollection! -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.Factory.get -> Microsoft.VisualStudio.Threading.JoinableTaskFactory! -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.IsMainThreadBlocked() -> bool -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.IsOnMainThread.get -> bool -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.JoinableTaskContextNode(Microsoft.VisualStudio.Threading.JoinableTaskContext! context) -> void -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.MainThread.get -> System.Threading.Thread! -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.RegisterOnHangDetected() -> System.IDisposable! -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.SuppressRelevance() -> Microsoft.VisualStudio.Threading.JoinableTaskContext.RevertRelevance -Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions -Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions.LongRunning = 1 -> Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions -Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions.None = 0 -> Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions -Microsoft.VisualStudio.Threading.JoinableTaskFactory -Microsoft.VisualStudio.Threading.JoinableTaskFactory.Add(Microsoft.VisualStudio.Threading.JoinableTask! joinable) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.Context.get -> Microsoft.VisualStudio.Threading.JoinableTaskContext! -Microsoft.VisualStudio.Threading.JoinableTaskFactory.HangDetectionTimeout.get -> System.TimeSpan -Microsoft.VisualStudio.Threading.JoinableTaskFactory.HangDetectionTimeout.set -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.IsWaitingOnLongRunningTask() -> bool -Microsoft.VisualStudio.Threading.JoinableTaskFactory.JoinableTaskFactory(Microsoft.VisualStudio.Threading.JoinableTaskCollection! collection) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.JoinableTaskFactory(Microsoft.VisualStudio.Threading.JoinableTaskContext! owner) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable -Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaiter -Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaiter -Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaiter.GetResult() -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.Run(System.Func! asyncMethod) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.Run(System.Func! asyncMethod, Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions creationOptions) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.Run(System.Func!>! asyncMethod) -> T -Microsoft.VisualStudio.Threading.JoinableTaskFactory.Run(System.Func!>! asyncMethod, Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions creationOptions) -> T -Microsoft.VisualStudio.Threading.JoinableTaskFactory.RunAsync(System.Func! asyncMethod) -> Microsoft.VisualStudio.Threading.JoinableTask! -Microsoft.VisualStudio.Threading.JoinableTaskFactory.RunAsync(System.Func! asyncMethod, Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions creationOptions) -> Microsoft.VisualStudio.Threading.JoinableTask! -Microsoft.VisualStudio.Threading.JoinableTaskFactory.RunAsync(System.Func!>! asyncMethod) -> Microsoft.VisualStudio.Threading.JoinableTask! -Microsoft.VisualStudio.Threading.JoinableTaskFactory.RunAsync(System.Func!>! asyncMethod, Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions creationOptions) -> Microsoft.VisualStudio.Threading.JoinableTask! -Microsoft.VisualStudio.Threading.JoinableTaskFactory.SwitchToMainThreadAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable -Microsoft.VisualStudio.Threading.JoinableTaskFactory.SwitchToMainThreadAsync(bool alwaysYield, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable -Microsoft.VisualStudio.Threading.JoinableTaskFactory.UnderlyingSynchronizationContext.get -> System.Threading.SynchronizationContext? -Microsoft.VisualStudio.Threading.NoMessagePumpSyncContext -Microsoft.VisualStudio.Threading.NoMessagePumpSyncContext.NoMessagePumpSyncContext() -> void -Microsoft.VisualStudio.Threading.ProgressWithCompletion -Microsoft.VisualStudio.Threading.ProgressWithCompletion.ProgressWithCompletion(System.Action! handler) -> void -Microsoft.VisualStudio.Threading.ProgressWithCompletion.ProgressWithCompletion(System.Action! handler, Microsoft.VisualStudio.Threading.JoinableTaskFactory? joinableTaskFactory) -> void -Microsoft.VisualStudio.Threading.ProgressWithCompletion.ProgressWithCompletion(System.Func! handler) -> void -Microsoft.VisualStudio.Threading.ProgressWithCompletion.ProgressWithCompletion(System.Func! handler, Microsoft.VisualStudio.Threading.JoinableTaskFactory? joinableTaskFactory) -> void -Microsoft.VisualStudio.Threading.ProgressWithCompletion.WaitAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.ProgressWithCompletion.WaitAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.ReentrantSemaphore -Microsoft.VisualStudio.Threading.ReentrantSemaphore.CurrentCount.get -> int -Microsoft.VisualStudio.Threading.ReentrantSemaphore.Dispose() -> void -Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode -Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode.Freeform = 3 -> Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode -Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode.NotAllowed = 0 -> Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode -Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode.NotRecognized = 1 -> Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode -Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode.Stack = 2 -> Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode -Microsoft.VisualStudio.Threading.ReentrantSemaphore.RevertRelevance -Microsoft.VisualStudio.Threading.ReentrantSemaphore.RevertRelevance.Dispose() -> void -Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters -Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters.Attributes = 2 -> Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters -Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters.Security = 8 -> Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters -Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters.Subkey = 1 -> Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters -Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters.Value = 4 -> Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters -Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext -Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.Frame -Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.Frame.Continue.get -> bool -Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.Frame.Continue.set -> void -Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.Frame.Frame() -> void -Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.PushFrame(Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.Frame! frame) -> void -Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.SingleThreadedSynchronizationContext() -> void -Microsoft.VisualStudio.Threading.SpecializedSyncContext -Microsoft.VisualStudio.Threading.SpecializedSyncContext.Dispose() -> void -Microsoft.VisualStudio.Threading.ThreadingTools -Microsoft.VisualStudio.Threading.TplExtensions -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaitable -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaiter -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaitable.NoThrowTaskAwaitable(System.Threading.Tasks.Task! task, bool captureContext) -> void -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaiter -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaiter.GetResult() -> void -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaiter.NoThrowTaskAwaiter(System.Threading.Tasks.Task! task, bool captureContext) -> void -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void -abstract Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.GetResourceAsync(TMoniker resourceMoniker, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -abstract Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.PrepareResourceForConcurrentAccessAsync(TResource! resource, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -abstract Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.PrepareResourceForExclusiveAccessAsync(TResource! resource, Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags lockFlags, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -abstract Microsoft.VisualStudio.Threading.ReentrantSemaphore.ExecuteAsync(System.Func! operation, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -abstract Microsoft.VisualStudio.Threading.ReentrantSemaphore.ExecuteAsync(System.Func>! operation, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -override Microsoft.VisualStudio.Threading.AsyncLazy.ToString() -> string! -override Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.OnExclusiveLockReleasedAsync() -> System.Threading.Tasks.Task! -override Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.OnUpgradeableReadLockReleased() -> void -override Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.Equals(object? obj) -> bool -override Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.GetHashCode() -> int -override Microsoft.VisualStudio.Threading.DelegatingJoinableTaskFactory.OnTransitionedToMainThread(Microsoft.VisualStudio.Threading.JoinableTask! joinableTask, bool canceled) -> void -override Microsoft.VisualStudio.Threading.DelegatingJoinableTaskFactory.OnTransitioningToMainThread(Microsoft.VisualStudio.Threading.JoinableTask! joinableTask) -> void -override Microsoft.VisualStudio.Threading.DelegatingJoinableTaskFactory.PostToUnderlyingSynchronizationContext(System.Threading.SendOrPostCallback! callback, object! state) -> void -override Microsoft.VisualStudio.Threading.DelegatingJoinableTaskFactory.WaitSynchronously(System.Threading.Tasks.Task! task) -> void -override Microsoft.VisualStudio.Threading.NoMessagePumpSyncContext.Wait(System.IntPtr[]! waitHandles, bool waitAll, int millisecondsTimeout) -> int -override Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.CreateCopy() -> System.Threading.SynchronizationContext! -override Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.Post(System.Threading.SendOrPostCallback! d, object? state) -> void -override Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.Send(System.Threading.SendOrPostCallback! d, object? state) -> void -static Microsoft.VisualStudio.Threading.AwaitExtensions.ConfigureAwait(this System.Runtime.CompilerServices.YieldAwaitable yieldAwaitable, bool continueOnCapturedContext) -> Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaitable -static Microsoft.VisualStudio.Threading.AwaitExtensions.ConfigureAwaitRunInline(this System.Threading.Tasks.Task! antecedent) -> Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable -static Microsoft.VisualStudio.Threading.AwaitExtensions.ConfigureAwaitRunInline(this System.Threading.Tasks.Task! antecedent) -> Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable -static Microsoft.VisualStudio.Threading.AwaitExtensions.GetAwaiter(this System.Threading.Tasks.TaskScheduler! scheduler) -> Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaiter -static Microsoft.VisualStudio.Threading.AwaitExtensions.GetAwaiter(this System.Threading.WaitHandle! handle) -> System.Runtime.CompilerServices.TaskAwaiter -static Microsoft.VisualStudio.Threading.AwaitExtensions.SwitchTo(this System.Threading.Tasks.TaskScheduler! scheduler, bool alwaysYield = false) -> Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaitable -static Microsoft.VisualStudio.Threading.AwaitExtensions.WaitForChangeAsync(this Microsoft.Win32.RegistryKey! registryKey, bool watchSubtree = true, Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters change = Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters.Subkey | Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters.Value, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.AwaitExtensions.WaitForExitAsync(this System.Diagnostics.Process! process, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombineWith(this System.Threading.CancellationToken original, System.Threading.CancellationToken other) -> Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken -static Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombineWith(this System.Threading.CancellationToken original, params System.Threading.CancellationToken[]! others) -> Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken -static Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.operator !=(Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken left, Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken right) -> bool -static Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.operator ==(Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken left, Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken right) -> bool -static Microsoft.VisualStudio.Threading.DispatcherExtensions.WithPriority(this Microsoft.VisualStudio.Threading.JoinableTaskFactory! joinableTaskFactory, System.Windows.Threading.Dispatcher! dispatcher, System.Windows.Threading.DispatcherPriority priority) -> Microsoft.VisualStudio.Threading.JoinableTaskFactory! -static Microsoft.VisualStudio.Threading.NoMessagePumpSyncContext.Default.get -> System.Threading.SynchronizationContext! -static Microsoft.VisualStudio.Threading.ReentrantSemaphore.Create(int initialCount = 1, Microsoft.VisualStudio.Threading.JoinableTaskContext? joinableTaskContext = null, Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode mode = Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode.NotAllowed) -> Microsoft.VisualStudio.Threading.ReentrantSemaphore! -static Microsoft.VisualStudio.Threading.SpecializedSyncContext.Apply(System.Threading.SynchronizationContext? syncContext, bool checkForChangesOnRevert = true) -> Microsoft.VisualStudio.Threading.SpecializedSyncContext -static Microsoft.VisualStudio.Threading.ThreadingTools.Apply(this System.Threading.SynchronizationContext? syncContext, bool checkForChangesOnRevert = true) -> Microsoft.VisualStudio.Threading.SpecializedSyncContext -static Microsoft.VisualStudio.Threading.ThreadingTools.ApplyChangeOptimistically(ref T hotLocation, TArg applyChangeArgument, System.Func! applyChange) -> bool -static Microsoft.VisualStudio.Threading.ThreadingTools.ApplyChangeOptimistically(ref T hotLocation, System.Func! applyChange) -> bool -static Microsoft.VisualStudio.Threading.ThreadingTools.WithCancellation(this System.Threading.Tasks.Task! task, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.ThreadingTools.WithCancellation(this System.Threading.Tasks.Task! task, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.AppendAction(this System.Threading.Tasks.Task! task, System.Action! action, System.Threading.Tasks.TaskContinuationOptions options = System.Threading.Tasks.TaskContinuationOptions.None, System.Threading.CancellationToken cancellation = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.ApplyResultTo(this System.Threading.Tasks.Task! task, System.Threading.Tasks.TaskCompletionSource! tcs) -> void -static Microsoft.VisualStudio.Threading.TplExtensions.ApplyResultTo(this System.Threading.Tasks.Task! task, System.Threading.Tasks.TaskCompletionSource! tcs) -> void -static Microsoft.VisualStudio.Threading.TplExtensions.AttachToParent(this System.Threading.Tasks.Task! task) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.AttachToParent(this System.Threading.Tasks.Task! task) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.FollowCancelableTaskToCompletion(System.Func!>! taskToFollow, System.Threading.CancellationToken ultimateCancellation, System.Threading.Tasks.TaskCompletionSource? taskThatFollows = null) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.Forget(this System.Threading.Tasks.Task? task) -> void -static Microsoft.VisualStudio.Threading.TplExtensions.InvokeAsync(this Microsoft.VisualStudio.Threading.AsyncEventHandler? handlers, object? sender, System.EventArgs! args) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.InvokeAsync(this Microsoft.VisualStudio.Threading.AsyncEventHandler? handlers, object? sender, TEventArgs args) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.NoThrowAwaitable(this System.Threading.Tasks.Task! task, bool captureContext = true) -> Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaitable -static Microsoft.VisualStudio.Threading.TplExtensions.ToApm(this System.Threading.Tasks.Task! task, System.AsyncCallback? callback, object? state) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.ToApm(this System.Threading.Tasks.Task! task, System.AsyncCallback? callback, object? state) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.ToTask(this System.Threading.WaitHandle! handle, int timeout = -1, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.WaitWithoutInlining(this System.Threading.Tasks.Task! task) -> void -static Microsoft.VisualStudio.Threading.TplExtensions.WithTimeout(this System.Threading.Tasks.Task! task, System.TimeSpan timeout) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.WithTimeout(this System.Threading.Tasks.Task! task, System.TimeSpan timeout) -> System.Threading.Tasks.Task! -static readonly Microsoft.VisualStudio.Threading.TplExtensions.CanceledTask -> System.Threading.Tasks.Task! -static readonly Microsoft.VisualStudio.Threading.TplExtensions.CompletedTask -> System.Threading.Tasks.Task! -static readonly Microsoft.VisualStudio.Threading.TplExtensions.FalseTask -> System.Threading.Tasks.Task! -static readonly Microsoft.VisualStudio.Threading.TplExtensions.TrueTask -> System.Threading.Tasks.Task! -virtual Microsoft.VisualStudio.Threading.AsyncQueue.InitialCapacity.get -> int -virtual Microsoft.VisualStudio.Threading.AsyncQueue.OnCompleted() -> void -virtual Microsoft.VisualStudio.Threading.AsyncQueue.OnDequeued(T value) -> void -virtual Microsoft.VisualStudio.Threading.AsyncQueue.OnEnqueued(T value, bool alreadyDispatched) -> void -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.CanCurrentThreadHoldActiveLock.get -> bool -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Dispose(bool disposing) -> void -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.GetHangReport() -> Microsoft.VisualStudio.Threading.HangReportContribution! -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.GetTaskSchedulerForReadLockRequest() -> System.Threading.Tasks.TaskScheduler! -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsUnsupportedSynchronizationContext.get -> bool -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.NoMessagePumpSynchronizationContext.get -> System.Threading.SynchronizationContext! -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.OnBeforeExclusiveLockReleasedAsync() -> System.Threading.Tasks.Task! -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.OnBeforeLockReleasedAsync(bool exclusiveLockRelease, Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle releasingLock) -> System.Threading.Tasks.Task! -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.OnCriticalFailure(System.Exception! ex) -> System.Exception! -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.OnExclusiveLockReleasedAsync() -> System.Threading.Tasks.Task! -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.OnUpgradeableReadLockReleased() -> void -virtual Microsoft.VisualStudio.Threading.AsyncSemaphore.Dispose(bool disposing) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskContext.CreateDefaultFactory() -> Microsoft.VisualStudio.Threading.JoinableTaskFactory! -virtual Microsoft.VisualStudio.Threading.JoinableTaskContext.CreateFactory(Microsoft.VisualStudio.Threading.JoinableTaskCollection! collection) -> Microsoft.VisualStudio.Threading.JoinableTaskFactory! -virtual Microsoft.VisualStudio.Threading.JoinableTaskContext.Dispose(bool disposing) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskContext.GetHangReport() -> Microsoft.VisualStudio.Threading.HangReportContribution! -virtual Microsoft.VisualStudio.Threading.JoinableTaskContext.NoMessagePumpSynchronizationContext.get -> System.Threading.SynchronizationContext! -virtual Microsoft.VisualStudio.Threading.JoinableTaskContext.OnFalseHangDetected(System.TimeSpan hangDuration, System.Guid hangId) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskContext.OnHangDetected(System.TimeSpan hangDuration, int notificationCount, System.Guid hangId) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskContextNode.CreateDefaultFactory() -> Microsoft.VisualStudio.Threading.JoinableTaskFactory! -virtual Microsoft.VisualStudio.Threading.JoinableTaskContextNode.CreateFactory(Microsoft.VisualStudio.Threading.JoinableTaskCollection! collection) -> Microsoft.VisualStudio.Threading.JoinableTaskFactory! -virtual Microsoft.VisualStudio.Threading.JoinableTaskContextNode.OnFalseHangDetected(System.TimeSpan hangDuration, System.Guid hangId) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskContextNode.OnHangDetected(Microsoft.VisualStudio.Threading.JoinableTaskContext.HangDetails! details) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskContextNode.OnHangDetected(System.TimeSpan hangDuration, int notificationCount, System.Guid hangId) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskFactory.OnTransitionedToMainThread(Microsoft.VisualStudio.Threading.JoinableTask! joinableTask, bool canceled) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskFactory.OnTransitioningToMainThread(Microsoft.VisualStudio.Threading.JoinableTask! joinableTask) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskFactory.PostToUnderlyingSynchronizationContext(System.Threading.SendOrPostCallback! callback, object! state) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskFactory.WaitSynchronously(System.Threading.Tasks.Task! task) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskFactory.WaitSynchronouslyCore(System.Threading.Tasks.Task! task) -> void -virtual Microsoft.VisualStudio.Threading.ProgressWithCompletion.Report(T value) -> void -virtual Microsoft.VisualStudio.Threading.ReentrantSemaphore.SuppressRelevance() -> Microsoft.VisualStudio.Threading.ReentrantSemaphore.RevertRelevance -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Releaser.DisposeAsync() -> System.Threading.Tasks.ValueTask -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceReleaser.DisposeAsync() -> System.Threading.Tasks.ValueTask -Microsoft.VisualStudio.Threading.NonConcurrentSynchronizationContext -Microsoft.VisualStudio.Threading.NonConcurrentSynchronizationContext.NonConcurrentSynchronizationContext(bool sticky) -> void -Microsoft.VisualStudio.Threading.NonConcurrentSynchronizationContext.UnhandledException -> System.EventHandler? -override Microsoft.VisualStudio.Threading.NonConcurrentSynchronizationContext.CreateCopy() -> System.Threading.SynchronizationContext! -override Microsoft.VisualStudio.Threading.NonConcurrentSynchronizationContext.Post(System.Threading.SendOrPostCallback! d, object? state) -> void -override Microsoft.VisualStudio.Threading.NonConcurrentSynchronizationContext.Send(System.Threading.SendOrPostCallback! d, object? state) -> void -static Microsoft.VisualStudio.Threading.TplExtensions.Forget(this System.Threading.Tasks.ValueTask task) -> void -static Microsoft.VisualStudio.Threading.TplExtensions.Forget(this System.Threading.Tasks.ValueTask task) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaitable -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaitable.AggregateExceptionAwaitable(System.Threading.Tasks.Task! task, bool continueOnCapturedContext) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaiter.AggregateExceptionAwaiter(System.Threading.Tasks.Task! task, bool continueOnCapturedContext) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaiter.GetResult() -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void -static Microsoft.VisualStudio.Threading.AwaitExtensions.ConfigureAwaitForAggregateException(this System.Threading.Tasks.Task! task, bool continueOnCapturedContext = true) -> Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaitable diff --git a/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt deleted file mode 100644 index e85d3c718..000000000 --- a/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,8 +0,0 @@ -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.AsyncReaderWriterLock(Microsoft.VisualStudio.Threading.JoinableTaskContext? joinableTaskContext, bool captureDiagnostics = false) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.AsyncReaderWriterResourceLock(Microsoft.VisualStudio.Threading.JoinableTaskContext? joinableTaskContext, bool captureDiagnostics) -> void -Microsoft.VisualStudio.Threading.JoinableTaskContext.IsMainThreadMaybeBlocked() -> bool -Microsoft.VisualStudio.Threading.SemaphoreFaultedException -Microsoft.VisualStudio.Threading.SemaphoreFaultedException.SemaphoreFaultedException() -> void -Microsoft.VisualStudio.Threading.IllegalSemaphoreUsageException -Microsoft.VisualStudio.Threading.IllegalSemaphoreUsageException.IllegalSemaphoreUsageException(string! message) -> void -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.DeadlockCheckTimeout.get -> System.TimeSpan diff --git a/src/Microsoft.VisualStudio.Threading/netcoreapp3.1/PublicAPI.Shipped.txt b/src/Microsoft.VisualStudio.Threading/netcoreapp3.1/PublicAPI.Shipped.txt deleted file mode 100644 index d8a42137d..000000000 --- a/src/Microsoft.VisualStudio.Threading/netcoreapp3.1/PublicAPI.Shipped.txt +++ /dev/null @@ -1,454 +0,0 @@ -#nullable enable -Microsoft.VisualStudio.Threading.AsyncAutoResetEvent -Microsoft.VisualStudio.Threading.AsyncAutoResetEvent.AsyncAutoResetEvent() -> void -Microsoft.VisualStudio.Threading.AsyncAutoResetEvent.AsyncAutoResetEvent(bool allowInliningAwaiters) -> void -Microsoft.VisualStudio.Threading.AsyncAutoResetEvent.Set() -> void -Microsoft.VisualStudio.Threading.AsyncAutoResetEvent.WaitAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncAutoResetEvent.WaitAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncBarrier -Microsoft.VisualStudio.Threading.AsyncBarrier.AsyncBarrier(int participants) -> void -Microsoft.VisualStudio.Threading.AsyncBarrier.SignalAndWait() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncCountdownEvent -Microsoft.VisualStudio.Threading.AsyncCountdownEvent.AsyncCountdownEvent(int initialCount) -> void -Microsoft.VisualStudio.Threading.AsyncCountdownEvent.Signal() -> void -Microsoft.VisualStudio.Threading.AsyncCountdownEvent.SignalAndWaitAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncCountdownEvent.SignalAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncCountdownEvent.WaitAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncEventHandler -Microsoft.VisualStudio.Threading.AsyncEventHandler -Microsoft.VisualStudio.Threading.AsyncLazy -Microsoft.VisualStudio.Threading.AsyncLazy.AsyncLazy(System.Func!>! valueFactory, Microsoft.VisualStudio.Threading.JoinableTaskFactory? joinableTaskFactory = null) -> void -Microsoft.VisualStudio.Threading.AsyncLazy.GetValue() -> T -Microsoft.VisualStudio.Threading.AsyncLazy.GetValue(System.Threading.CancellationToken cancellationToken) -> T -Microsoft.VisualStudio.Threading.AsyncLazy.GetValueAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncLazy.GetValueAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncLazy.IsValueCreated.get -> bool -Microsoft.VisualStudio.Threading.AsyncLazy.IsValueFactoryCompleted.get -> bool -Microsoft.VisualStudio.Threading.AsyncLazyInitializer -Microsoft.VisualStudio.Threading.AsyncLazyInitializer.AsyncLazyInitializer(System.Func! action, Microsoft.VisualStudio.Threading.JoinableTaskFactory? joinableTaskFactory = null) -> void -Microsoft.VisualStudio.Threading.AsyncLazyInitializer.Initialize(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void -Microsoft.VisualStudio.Threading.AsyncLazyInitializer.InitializeAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncLazyInitializer.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AsyncLazyInitializer.IsCompletedSuccessfully.get -> bool -Microsoft.VisualStudio.Threading.AsyncLocal -Microsoft.VisualStudio.Threading.AsyncLocal.AsyncLocal() -> void -Microsoft.VisualStudio.Threading.AsyncLocal.Value.get -> T? -Microsoft.VisualStudio.Threading.AsyncLocal.Value.set -> void -Microsoft.VisualStudio.Threading.AsyncManualResetEvent -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.AsyncManualResetEvent(bool initialState = false, bool allowInliningAwaiters = false) -> void -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.GetAwaiter() -> System.Runtime.CompilerServices.TaskAwaiter -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.IsSet.get -> bool -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.PulseAll() -> void -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.PulseAllAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.Reset() -> void -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.Set() -> void -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.SetAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.WaitAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.WaitAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncQueue -Microsoft.VisualStudio.Threading.AsyncQueue.AsyncQueue() -> void -Microsoft.VisualStudio.Threading.AsyncQueue.Complete() -> void -Microsoft.VisualStudio.Threading.AsyncQueue.Completion.get -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncQueue.Count.get -> int -Microsoft.VisualStudio.Threading.AsyncQueue.DequeueAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncQueue.Enqueue(T value) -> void -Microsoft.VisualStudio.Threading.AsyncQueue.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AsyncQueue.IsEmpty.get -> bool -Microsoft.VisualStudio.Threading.AsyncQueue.Peek() -> T -Microsoft.VisualStudio.Threading.AsyncQueue.SyncRoot.get -> object! -Microsoft.VisualStudio.Threading.AsyncQueue.TryDequeue(System.Predicate! valueCheck, out T value) -> bool -Microsoft.VisualStudio.Threading.AsyncQueue.TryDequeue(out T value) -> bool -Microsoft.VisualStudio.Threading.AsyncQueue.TryEnqueue(T value) -> bool -Microsoft.VisualStudio.Threading.AsyncQueue.TryPeek(out T value) -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.AmbientLock.get -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.AsyncReaderWriterLock() -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.AsyncReaderWriterLock(bool captureDiagnostics) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaiter! -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaiter -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaiter.GetResult() -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Releaser -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaiter.UnsafeOnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.CaptureDiagnostics.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.CaptureDiagnostics.set -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Complete() -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Completion.get -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Dispose() -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.GetAggregateLockFlags() -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.HideLocks() -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Suppression -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsAnyLockHeld.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsAnyPassiveLockHeld.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsPassiveReadLockHeld.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsPassiveUpgradeableReadLockHeld.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsPassiveWriteLockHeld.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsReadLockHeld.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsUpgradeableReadLockHeld.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsWriteLockHeld.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags.None = 0 -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags.StickyWrite = 1 -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.Data.get -> object? -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.Data.set -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.Flags.get -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.HasReadLock.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.HasUpgradeableReadLock.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.HasWriteLock.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.IsActive.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.IsReadLock.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.IsUpgradeableReadLock.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.IsValid.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.IsWriteLock.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.NestingLock.get -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockStackContains(Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags flags, Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle handle) -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.OnBeforeWriteLockReleased(System.Func! action) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.OnCriticalFailure(string! message) -> System.Exception! -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.ReadLockAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Releaser -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Releaser.Dispose() -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Releaser.ReleaseAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Suppression -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Suppression.Dispose() -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.SyncObject.get -> object! -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.UpgradeableReadLockAsync(Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.UpgradeableReadLockAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.WriteLockAsync(Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.WriteLockAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.AsyncReaderWriterResourceLock() -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.AsyncReaderWriterResourceLock(bool captureDiagnostics) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.GetAggregateLockFlags() -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags.None = 0 -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags.SkipInitialPreparation = 4096 -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags.StickyWrite = 1 -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ReadLockAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaiter -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaiter -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaiter.GetResult() -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceReleaser -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceReleaser -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceReleaser.Dispose() -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceReleaser.GetResourceAsync(TMoniker resourceMoniker, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceReleaser.ReleaseAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.SetAllResourcesToUnknownState() -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.SetResourceAsAccessed(System.Func! resourceCheck, object? state) -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.SetResourceAsAccessed(TResource! resource) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.UpgradeableReadLockAsync(Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.UpgradeableReadLockAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.WriteLockAsync(Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.WriteLockAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaitable -Microsoft.VisualStudio.Threading.AsyncSemaphore -Microsoft.VisualStudio.Threading.AsyncSemaphore.AsyncSemaphore(int initialCount) -> void -Microsoft.VisualStudio.Threading.AsyncSemaphore.CurrentCount.get -> int -Microsoft.VisualStudio.Threading.AsyncSemaphore.Dispose() -> void -Microsoft.VisualStudio.Threading.AsyncSemaphore.EnterAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncSemaphore.EnterAsync(System.TimeSpan timeout, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncSemaphore.EnterAsync(int timeout, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncSemaphore.Releaser -Microsoft.VisualStudio.Threading.AsyncSemaphore.Releaser.Dispose() -> void -Microsoft.VisualStudio.Threading.AwaitExtensions -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaitable -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaitable.ConfiguredTaskYieldAwaitable(bool continueOnCapturedContext) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaiter.ConfiguredTaskYieldAwaiter(bool continueOnCapturedContext) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaiter.GetResult() -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable.ExecuteContinuationSynchronouslyAwaitable(System.Threading.Tasks.Task! antecedent) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable.ExecuteContinuationSynchronouslyAwaitable(System.Threading.Tasks.Task! antecedent) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter.ExecuteContinuationSynchronouslyAwaiter(System.Threading.Tasks.Task! antecedent) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter.GetResult() -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter.ExecuteContinuationSynchronouslyAwaiter(System.Threading.Tasks.Task! antecedent) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter.GetResult() -> T -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaitable -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaitable.TaskSchedulerAwaitable(System.Threading.Tasks.TaskScheduler! taskScheduler, bool alwaysYield = false) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaiter.GetResult() -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaiter.TaskSchedulerAwaiter(System.Threading.Tasks.TaskScheduler! scheduler, bool alwaysYield = false) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.CancellationTokenExtensions -Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken -Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.CombinedCancellationToken(System.Threading.CancellationToken cancellationToken) -> void -Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.CombinedCancellationToken(System.Threading.CancellationTokenSource! cancellationTokenSource) -> void -Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.Dispose() -> void -Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.Equals(Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken other) -> bool -Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.Token.get -> System.Threading.CancellationToken -Microsoft.VisualStudio.Threading.DelegatingJoinableTaskFactory -Microsoft.VisualStudio.Threading.DelegatingJoinableTaskFactory.DelegatingJoinableTaskFactory(Microsoft.VisualStudio.Threading.JoinableTaskFactory! innerFactory) -> void -Microsoft.VisualStudio.Threading.HangReportContribution -Microsoft.VisualStudio.Threading.HangReportContribution.Content.get -> string! -Microsoft.VisualStudio.Threading.HangReportContribution.ContentName.get -> string? -Microsoft.VisualStudio.Threading.HangReportContribution.ContentType.get -> string? -Microsoft.VisualStudio.Threading.HangReportContribution.HangReportContribution(string! content, string? contentType, string? contentName) -> void -Microsoft.VisualStudio.Threading.HangReportContribution.HangReportContribution(string! content, string? contentType, string? contentName, params Microsoft.VisualStudio.Threading.HangReportContribution![]? nestedReports) -> void -Microsoft.VisualStudio.Threading.HangReportContribution.NestedReports.get -> System.Collections.Generic.IReadOnlyCollection? -Microsoft.VisualStudio.Threading.IAsyncDisposable -Microsoft.VisualStudio.Threading.IAsyncDisposable.DisposeAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.IHangReportContributor -Microsoft.VisualStudio.Threading.IHangReportContributor.GetHangReport() -> Microsoft.VisualStudio.Threading.HangReportContribution! -Microsoft.VisualStudio.Threading.JoinableTask -Microsoft.VisualStudio.Threading.JoinableTask.GetAwaiter() -> System.Runtime.CompilerServices.TaskAwaiter -Microsoft.VisualStudio.Threading.JoinableTask.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.JoinableTask.Join(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void -Microsoft.VisualStudio.Threading.JoinableTask.JoinAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.JoinableTask.Task.get -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.JoinableTask -Microsoft.VisualStudio.Threading.JoinableTask.GetAwaiter() -> System.Runtime.CompilerServices.TaskAwaiter -Microsoft.VisualStudio.Threading.JoinableTask.Join(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> T -Microsoft.VisualStudio.Threading.JoinableTask.JoinAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.JoinableTask.Task.get -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.JoinableTaskCollection -Microsoft.VisualStudio.Threading.JoinableTaskCollection.Add(Microsoft.VisualStudio.Threading.JoinableTask! joinableTask) -> void -Microsoft.VisualStudio.Threading.JoinableTaskCollection.Contains(Microsoft.VisualStudio.Threading.JoinableTask! joinableTask) -> bool -Microsoft.VisualStudio.Threading.JoinableTaskCollection.Context.get -> Microsoft.VisualStudio.Threading.JoinableTaskContext! -Microsoft.VisualStudio.Threading.JoinableTaskCollection.DisplayName.get -> string? -Microsoft.VisualStudio.Threading.JoinableTaskCollection.DisplayName.set -> void -Microsoft.VisualStudio.Threading.JoinableTaskCollection.GetEnumerator() -> System.Collections.Generic.IEnumerator! -Microsoft.VisualStudio.Threading.JoinableTaskCollection.Join() -> Microsoft.VisualStudio.Threading.JoinableTaskCollection.JoinRelease -Microsoft.VisualStudio.Threading.JoinableTaskCollection.JoinRelease -Microsoft.VisualStudio.Threading.JoinableTaskCollection.JoinRelease.Dispose() -> void -Microsoft.VisualStudio.Threading.JoinableTaskCollection.JoinTillEmptyAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.JoinableTaskCollection.JoinTillEmptyAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.JoinableTaskCollection.JoinableTaskCollection(Microsoft.VisualStudio.Threading.JoinableTaskContext! context, bool refCountAddedJobs = false) -> void -Microsoft.VisualStudio.Threading.JoinableTaskCollection.Remove(Microsoft.VisualStudio.Threading.JoinableTask! joinableTask) -> void -Microsoft.VisualStudio.Threading.JoinableTaskContext -Microsoft.VisualStudio.Threading.JoinableTaskContext.CreateCollection() -> Microsoft.VisualStudio.Threading.JoinableTaskCollection! -Microsoft.VisualStudio.Threading.JoinableTaskContext.Dispose() -> void -Microsoft.VisualStudio.Threading.JoinableTaskContext.Factory.get -> Microsoft.VisualStudio.Threading.JoinableTaskFactory! -Microsoft.VisualStudio.Threading.JoinableTaskContext.HangDetails -Microsoft.VisualStudio.Threading.JoinableTaskContext.HangDetails.EntryMethod.get -> System.Reflection.MethodInfo? -Microsoft.VisualStudio.Threading.JoinableTaskContext.HangDetails.HangDetails(System.TimeSpan hangDuration, int notificationCount, System.Guid hangId, System.Reflection.MethodInfo? entryMethod) -> void -Microsoft.VisualStudio.Threading.JoinableTaskContext.HangDetails.HangDuration.get -> System.TimeSpan -Microsoft.VisualStudio.Threading.JoinableTaskContext.HangDetails.HangId.get -> System.Guid -Microsoft.VisualStudio.Threading.JoinableTaskContext.HangDetails.NotificationCount.get -> int -Microsoft.VisualStudio.Threading.JoinableTaskContext.IsMainThreadBlocked() -> bool -Microsoft.VisualStudio.Threading.JoinableTaskContext.IsOnMainThread.get -> bool -Microsoft.VisualStudio.Threading.JoinableTaskContext.IsWithinJoinableTask.get -> bool -Microsoft.VisualStudio.Threading.JoinableTaskContext.JoinableTaskContext() -> void -Microsoft.VisualStudio.Threading.JoinableTaskContext.JoinableTaskContext(System.Threading.Thread? mainThread = null, System.Threading.SynchronizationContext? synchronizationContext = null) -> void -Microsoft.VisualStudio.Threading.JoinableTaskContext.MainThread.get -> System.Threading.Thread! -Microsoft.VisualStudio.Threading.JoinableTaskContext.RevertRelevance -Microsoft.VisualStudio.Threading.JoinableTaskContext.RevertRelevance.Dispose() -> void -Microsoft.VisualStudio.Threading.JoinableTaskContext.SuppressRelevance() -> Microsoft.VisualStudio.Threading.JoinableTaskContext.RevertRelevance -Microsoft.VisualStudio.Threading.JoinableTaskContextException -Microsoft.VisualStudio.Threading.JoinableTaskContextException.JoinableTaskContextException() -> void -Microsoft.VisualStudio.Threading.JoinableTaskContextException.JoinableTaskContextException(System.Runtime.Serialization.SerializationInfo! info, System.Runtime.Serialization.StreamingContext context) -> void -Microsoft.VisualStudio.Threading.JoinableTaskContextException.JoinableTaskContextException(string? message) -> void -Microsoft.VisualStudio.Threading.JoinableTaskContextException.JoinableTaskContextException(string? message, System.Exception? inner) -> void -Microsoft.VisualStudio.Threading.JoinableTaskContextNode -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.Context.get -> Microsoft.VisualStudio.Threading.JoinableTaskContext! -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.CreateCollection() -> Microsoft.VisualStudio.Threading.JoinableTaskCollection! -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.Factory.get -> Microsoft.VisualStudio.Threading.JoinableTaskFactory! -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.IsMainThreadBlocked() -> bool -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.IsOnMainThread.get -> bool -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.JoinableTaskContextNode(Microsoft.VisualStudio.Threading.JoinableTaskContext! context) -> void -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.MainThread.get -> System.Threading.Thread! -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.RegisterOnHangDetected() -> System.IDisposable! -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.SuppressRelevance() -> Microsoft.VisualStudio.Threading.JoinableTaskContext.RevertRelevance -Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions -Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions.LongRunning = 1 -> Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions -Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions.None = 0 -> Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions -Microsoft.VisualStudio.Threading.JoinableTaskFactory -Microsoft.VisualStudio.Threading.JoinableTaskFactory.Add(Microsoft.VisualStudio.Threading.JoinableTask! joinable) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.Context.get -> Microsoft.VisualStudio.Threading.JoinableTaskContext! -Microsoft.VisualStudio.Threading.JoinableTaskFactory.HangDetectionTimeout.get -> System.TimeSpan -Microsoft.VisualStudio.Threading.JoinableTaskFactory.HangDetectionTimeout.set -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.IsWaitingOnLongRunningTask() -> bool -Microsoft.VisualStudio.Threading.JoinableTaskFactory.JoinableTaskFactory(Microsoft.VisualStudio.Threading.JoinableTaskCollection! collection) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.JoinableTaskFactory(Microsoft.VisualStudio.Threading.JoinableTaskContext! owner) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable -Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaiter -Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaiter -Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaiter.GetResult() -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.Run(System.Func! asyncMethod) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.Run(System.Func! asyncMethod, Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions creationOptions) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.Run(System.Func!>! asyncMethod) -> T -Microsoft.VisualStudio.Threading.JoinableTaskFactory.Run(System.Func!>! asyncMethod, Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions creationOptions) -> T -Microsoft.VisualStudio.Threading.JoinableTaskFactory.RunAsync(System.Func! asyncMethod) -> Microsoft.VisualStudio.Threading.JoinableTask! -Microsoft.VisualStudio.Threading.JoinableTaskFactory.RunAsync(System.Func! asyncMethod, Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions creationOptions) -> Microsoft.VisualStudio.Threading.JoinableTask! -Microsoft.VisualStudio.Threading.JoinableTaskFactory.RunAsync(System.Func!>! asyncMethod) -> Microsoft.VisualStudio.Threading.JoinableTask! -Microsoft.VisualStudio.Threading.JoinableTaskFactory.RunAsync(System.Func!>! asyncMethod, Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions creationOptions) -> Microsoft.VisualStudio.Threading.JoinableTask! -Microsoft.VisualStudio.Threading.JoinableTaskFactory.SwitchToMainThreadAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable -Microsoft.VisualStudio.Threading.JoinableTaskFactory.SwitchToMainThreadAsync(bool alwaysYield, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable -Microsoft.VisualStudio.Threading.JoinableTaskFactory.UnderlyingSynchronizationContext.get -> System.Threading.SynchronizationContext? -Microsoft.VisualStudio.Threading.NoMessagePumpSyncContext -Microsoft.VisualStudio.Threading.NoMessagePumpSyncContext.NoMessagePumpSyncContext() -> void -Microsoft.VisualStudio.Threading.ProgressWithCompletion -Microsoft.VisualStudio.Threading.ProgressWithCompletion.ProgressWithCompletion(System.Action! handler) -> void -Microsoft.VisualStudio.Threading.ProgressWithCompletion.ProgressWithCompletion(System.Action! handler, Microsoft.VisualStudio.Threading.JoinableTaskFactory? joinableTaskFactory) -> void -Microsoft.VisualStudio.Threading.ProgressWithCompletion.ProgressWithCompletion(System.Func! handler) -> void -Microsoft.VisualStudio.Threading.ProgressWithCompletion.ProgressWithCompletion(System.Func! handler, Microsoft.VisualStudio.Threading.JoinableTaskFactory? joinableTaskFactory) -> void -Microsoft.VisualStudio.Threading.ProgressWithCompletion.WaitAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.ProgressWithCompletion.WaitAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.ReentrantSemaphore -Microsoft.VisualStudio.Threading.ReentrantSemaphore.CurrentCount.get -> int -Microsoft.VisualStudio.Threading.ReentrantSemaphore.Dispose() -> void -Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode -Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode.Freeform = 3 -> Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode -Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode.NotAllowed = 0 -> Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode -Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode.NotRecognized = 1 -> Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode -Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode.Stack = 2 -> Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode -Microsoft.VisualStudio.Threading.ReentrantSemaphore.RevertRelevance -Microsoft.VisualStudio.Threading.ReentrantSemaphore.RevertRelevance.Dispose() -> void -Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters -Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters.Attributes = 2 -> Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters -Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters.Security = 8 -> Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters -Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters.Subkey = 1 -> Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters -Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters.Value = 4 -> Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters -Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext -Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.Frame -Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.Frame.Continue.get -> bool -Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.Frame.Continue.set -> void -Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.Frame.Frame() -> void -Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.PushFrame(Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.Frame! frame) -> void -Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.SingleThreadedSynchronizationContext() -> void -Microsoft.VisualStudio.Threading.SpecializedSyncContext -Microsoft.VisualStudio.Threading.SpecializedSyncContext.Dispose() -> void -Microsoft.VisualStudio.Threading.ThreadingTools -Microsoft.VisualStudio.Threading.TplExtensions -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaitable -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaiter -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaitable.NoThrowTaskAwaitable(System.Threading.Tasks.Task! task, bool captureContext) -> void -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaiter -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaiter.GetResult() -> void -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaiter.NoThrowTaskAwaiter(System.Threading.Tasks.Task! task, bool captureContext) -> void -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void -abstract Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.GetResourceAsync(TMoniker resourceMoniker, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -abstract Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.PrepareResourceForConcurrentAccessAsync(TResource! resource, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -abstract Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.PrepareResourceForExclusiveAccessAsync(TResource! resource, Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags lockFlags, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -abstract Microsoft.VisualStudio.Threading.ReentrantSemaphore.ExecuteAsync(System.Func! operation, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -abstract Microsoft.VisualStudio.Threading.ReentrantSemaphore.ExecuteAsync(System.Func>! operation, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -override Microsoft.VisualStudio.Threading.AsyncLazy.ToString() -> string! -override Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.OnExclusiveLockReleasedAsync() -> System.Threading.Tasks.Task! -override Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.OnUpgradeableReadLockReleased() -> void -override Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.Equals(object? obj) -> bool -override Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.GetHashCode() -> int -override Microsoft.VisualStudio.Threading.DelegatingJoinableTaskFactory.OnTransitionedToMainThread(Microsoft.VisualStudio.Threading.JoinableTask! joinableTask, bool canceled) -> void -override Microsoft.VisualStudio.Threading.DelegatingJoinableTaskFactory.OnTransitioningToMainThread(Microsoft.VisualStudio.Threading.JoinableTask! joinableTask) -> void -override Microsoft.VisualStudio.Threading.DelegatingJoinableTaskFactory.PostToUnderlyingSynchronizationContext(System.Threading.SendOrPostCallback! callback, object! state) -> void -override Microsoft.VisualStudio.Threading.DelegatingJoinableTaskFactory.WaitSynchronously(System.Threading.Tasks.Task! task) -> void -override Microsoft.VisualStudio.Threading.NoMessagePumpSyncContext.Wait(System.IntPtr[]! waitHandles, bool waitAll, int millisecondsTimeout) -> int -override Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.CreateCopy() -> System.Threading.SynchronizationContext! -override Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.Post(System.Threading.SendOrPostCallback! d, object? state) -> void -override Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.Send(System.Threading.SendOrPostCallback! d, object? state) -> void -static Microsoft.VisualStudio.Threading.AwaitExtensions.ConfigureAwait(this System.Runtime.CompilerServices.YieldAwaitable yieldAwaitable, bool continueOnCapturedContext) -> Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaitable -static Microsoft.VisualStudio.Threading.AwaitExtensions.ConfigureAwaitRunInline(this System.Threading.Tasks.Task! antecedent) -> Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable -static Microsoft.VisualStudio.Threading.AwaitExtensions.ConfigureAwaitRunInline(this System.Threading.Tasks.Task! antecedent) -> Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable -static Microsoft.VisualStudio.Threading.AwaitExtensions.GetAwaiter(this System.Threading.Tasks.TaskScheduler! scheduler) -> Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaiter -static Microsoft.VisualStudio.Threading.AwaitExtensions.GetAwaiter(this System.Threading.WaitHandle! handle) -> System.Runtime.CompilerServices.TaskAwaiter -static Microsoft.VisualStudio.Threading.AwaitExtensions.SwitchTo(this System.Threading.Tasks.TaskScheduler! scheduler, bool alwaysYield = false) -> Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaitable -static Microsoft.VisualStudio.Threading.AwaitExtensions.WaitForChangeAsync(this Microsoft.Win32.RegistryKey! registryKey, bool watchSubtree = true, Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters change = Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters.Subkey | Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters.Value, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.AwaitExtensions.WaitForExitAsync(this System.Diagnostics.Process! process, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombineWith(this System.Threading.CancellationToken original, System.Threading.CancellationToken other) -> Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken -static Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombineWith(this System.Threading.CancellationToken original, params System.Threading.CancellationToken[]! others) -> Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken -static Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.operator !=(Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken left, Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken right) -> bool -static Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.operator ==(Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken left, Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken right) -> bool -static Microsoft.VisualStudio.Threading.NoMessagePumpSyncContext.Default.get -> System.Threading.SynchronizationContext! -static Microsoft.VisualStudio.Threading.ReentrantSemaphore.Create(int initialCount = 1, Microsoft.VisualStudio.Threading.JoinableTaskContext? joinableTaskContext = null, Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode mode = Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode.NotAllowed) -> Microsoft.VisualStudio.Threading.ReentrantSemaphore! -static Microsoft.VisualStudio.Threading.SpecializedSyncContext.Apply(System.Threading.SynchronizationContext? syncContext, bool checkForChangesOnRevert = true) -> Microsoft.VisualStudio.Threading.SpecializedSyncContext -static Microsoft.VisualStudio.Threading.ThreadingTools.Apply(this System.Threading.SynchronizationContext? syncContext, bool checkForChangesOnRevert = true) -> Microsoft.VisualStudio.Threading.SpecializedSyncContext -static Microsoft.VisualStudio.Threading.ThreadingTools.ApplyChangeOptimistically(ref T hotLocation, TArg applyChangeArgument, System.Func! applyChange) -> bool -static Microsoft.VisualStudio.Threading.ThreadingTools.ApplyChangeOptimistically(ref T hotLocation, System.Func! applyChange) -> bool -static Microsoft.VisualStudio.Threading.ThreadingTools.WithCancellation(this System.Threading.Tasks.Task! task, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.ThreadingTools.WithCancellation(this System.Threading.Tasks.Task! task, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.AppendAction(this System.Threading.Tasks.Task! task, System.Action! action, System.Threading.Tasks.TaskContinuationOptions options = System.Threading.Tasks.TaskContinuationOptions.None, System.Threading.CancellationToken cancellation = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.ApplyResultTo(this System.Threading.Tasks.Task! task, System.Threading.Tasks.TaskCompletionSource! tcs) -> void -static Microsoft.VisualStudio.Threading.TplExtensions.ApplyResultTo(this System.Threading.Tasks.Task! task, System.Threading.Tasks.TaskCompletionSource! tcs) -> void -static Microsoft.VisualStudio.Threading.TplExtensions.AttachToParent(this System.Threading.Tasks.Task! task) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.AttachToParent(this System.Threading.Tasks.Task! task) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.FollowCancelableTaskToCompletion(System.Func!>! taskToFollow, System.Threading.CancellationToken ultimateCancellation, System.Threading.Tasks.TaskCompletionSource? taskThatFollows = null) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.Forget(this System.Threading.Tasks.Task? task) -> void -static Microsoft.VisualStudio.Threading.TplExtensions.InvokeAsync(this Microsoft.VisualStudio.Threading.AsyncEventHandler? handlers, object? sender, System.EventArgs! args) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.InvokeAsync(this Microsoft.VisualStudio.Threading.AsyncEventHandler? handlers, object? sender, TEventArgs args) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.NoThrowAwaitable(this System.Threading.Tasks.Task! task, bool captureContext = true) -> Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaitable -static Microsoft.VisualStudio.Threading.TplExtensions.ToApm(this System.Threading.Tasks.Task! task, System.AsyncCallback? callback, object? state) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.ToApm(this System.Threading.Tasks.Task! task, System.AsyncCallback? callback, object? state) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.ToTask(this System.Threading.WaitHandle! handle, int timeout = -1, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.WaitWithoutInlining(this System.Threading.Tasks.Task! task) -> void -static Microsoft.VisualStudio.Threading.TplExtensions.WithTimeout(this System.Threading.Tasks.Task! task, System.TimeSpan timeout) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.WithTimeout(this System.Threading.Tasks.Task! task, System.TimeSpan timeout) -> System.Threading.Tasks.Task! -static readonly Microsoft.VisualStudio.Threading.TplExtensions.CanceledTask -> System.Threading.Tasks.Task! -static readonly Microsoft.VisualStudio.Threading.TplExtensions.CompletedTask -> System.Threading.Tasks.Task! -static readonly Microsoft.VisualStudio.Threading.TplExtensions.FalseTask -> System.Threading.Tasks.Task! -static readonly Microsoft.VisualStudio.Threading.TplExtensions.TrueTask -> System.Threading.Tasks.Task! -virtual Microsoft.VisualStudio.Threading.AsyncQueue.InitialCapacity.get -> int -virtual Microsoft.VisualStudio.Threading.AsyncQueue.OnCompleted() -> void -virtual Microsoft.VisualStudio.Threading.AsyncQueue.OnDequeued(T value) -> void -virtual Microsoft.VisualStudio.Threading.AsyncQueue.OnEnqueued(T value, bool alreadyDispatched) -> void -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.CanCurrentThreadHoldActiveLock.get -> bool -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Dispose(bool disposing) -> void -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.GetHangReport() -> Microsoft.VisualStudio.Threading.HangReportContribution! -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.GetTaskSchedulerForReadLockRequest() -> System.Threading.Tasks.TaskScheduler! -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsUnsupportedSynchronizationContext.get -> bool -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.NoMessagePumpSynchronizationContext.get -> System.Threading.SynchronizationContext! -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.OnBeforeExclusiveLockReleasedAsync() -> System.Threading.Tasks.Task! -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.OnBeforeLockReleasedAsync(bool exclusiveLockRelease, Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle releasingLock) -> System.Threading.Tasks.Task! -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.OnCriticalFailure(System.Exception! ex) -> System.Exception! -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.OnExclusiveLockReleasedAsync() -> System.Threading.Tasks.Task! -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.OnUpgradeableReadLockReleased() -> void -virtual Microsoft.VisualStudio.Threading.AsyncSemaphore.Dispose(bool disposing) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskContext.CreateDefaultFactory() -> Microsoft.VisualStudio.Threading.JoinableTaskFactory! -virtual Microsoft.VisualStudio.Threading.JoinableTaskContext.CreateFactory(Microsoft.VisualStudio.Threading.JoinableTaskCollection! collection) -> Microsoft.VisualStudio.Threading.JoinableTaskFactory! -virtual Microsoft.VisualStudio.Threading.JoinableTaskContext.Dispose(bool disposing) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskContext.GetHangReport() -> Microsoft.VisualStudio.Threading.HangReportContribution! -virtual Microsoft.VisualStudio.Threading.JoinableTaskContext.NoMessagePumpSynchronizationContext.get -> System.Threading.SynchronizationContext! -virtual Microsoft.VisualStudio.Threading.JoinableTaskContext.OnFalseHangDetected(System.TimeSpan hangDuration, System.Guid hangId) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskContext.OnHangDetected(System.TimeSpan hangDuration, int notificationCount, System.Guid hangId) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskContextNode.CreateDefaultFactory() -> Microsoft.VisualStudio.Threading.JoinableTaskFactory! -virtual Microsoft.VisualStudio.Threading.JoinableTaskContextNode.CreateFactory(Microsoft.VisualStudio.Threading.JoinableTaskCollection! collection) -> Microsoft.VisualStudio.Threading.JoinableTaskFactory! -virtual Microsoft.VisualStudio.Threading.JoinableTaskContextNode.OnFalseHangDetected(System.TimeSpan hangDuration, System.Guid hangId) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskContextNode.OnHangDetected(Microsoft.VisualStudio.Threading.JoinableTaskContext.HangDetails! details) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskContextNode.OnHangDetected(System.TimeSpan hangDuration, int notificationCount, System.Guid hangId) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskFactory.OnTransitionedToMainThread(Microsoft.VisualStudio.Threading.JoinableTask! joinableTask, bool canceled) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskFactory.OnTransitioningToMainThread(Microsoft.VisualStudio.Threading.JoinableTask! joinableTask) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskFactory.PostToUnderlyingSynchronizationContext(System.Threading.SendOrPostCallback! callback, object! state) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskFactory.WaitSynchronously(System.Threading.Tasks.Task! task) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskFactory.WaitSynchronouslyCore(System.Threading.Tasks.Task! task) -> void -virtual Microsoft.VisualStudio.Threading.ProgressWithCompletion.Report(T value) -> void -virtual Microsoft.VisualStudio.Threading.ReentrantSemaphore.SuppressRelevance() -> Microsoft.VisualStudio.Threading.ReentrantSemaphore.RevertRelevance -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Releaser.DisposeAsync() -> System.Threading.Tasks.ValueTask -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceReleaser.DisposeAsync() -> System.Threading.Tasks.ValueTask -Microsoft.VisualStudio.Threading.NonConcurrentSynchronizationContext -Microsoft.VisualStudio.Threading.NonConcurrentSynchronizationContext.NonConcurrentSynchronizationContext(bool sticky) -> void -Microsoft.VisualStudio.Threading.NonConcurrentSynchronizationContext.UnhandledException -> System.EventHandler? -override Microsoft.VisualStudio.Threading.NonConcurrentSynchronizationContext.CreateCopy() -> System.Threading.SynchronizationContext! -override Microsoft.VisualStudio.Threading.NonConcurrentSynchronizationContext.Post(System.Threading.SendOrPostCallback! d, object? state) -> void -override Microsoft.VisualStudio.Threading.NonConcurrentSynchronizationContext.Send(System.Threading.SendOrPostCallback! d, object? state) -> void -static Microsoft.VisualStudio.Threading.TplExtensions.Forget(this System.Threading.Tasks.ValueTask task) -> void -static Microsoft.VisualStudio.Threading.TplExtensions.Forget(this System.Threading.Tasks.ValueTask task) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaitable -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaitable.AggregateExceptionAwaitable(System.Threading.Tasks.Task! task, bool continueOnCapturedContext) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaiter.AggregateExceptionAwaiter(System.Threading.Tasks.Task! task, bool continueOnCapturedContext) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaiter.GetResult() -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void -static Microsoft.VisualStudio.Threading.AwaitExtensions.ConfigureAwaitForAggregateException(this System.Threading.Tasks.Task! task, bool continueOnCapturedContext = true) -> Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaitable diff --git a/src/Microsoft.VisualStudio.Threading/netcoreapp3.1/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.Threading/netcoreapp3.1/PublicAPI.Unshipped.txt deleted file mode 100644 index e85d3c718..000000000 --- a/src/Microsoft.VisualStudio.Threading/netcoreapp3.1/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,8 +0,0 @@ -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.AsyncReaderWriterLock(Microsoft.VisualStudio.Threading.JoinableTaskContext? joinableTaskContext, bool captureDiagnostics = false) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.AsyncReaderWriterResourceLock(Microsoft.VisualStudio.Threading.JoinableTaskContext? joinableTaskContext, bool captureDiagnostics) -> void -Microsoft.VisualStudio.Threading.JoinableTaskContext.IsMainThreadMaybeBlocked() -> bool -Microsoft.VisualStudio.Threading.SemaphoreFaultedException -Microsoft.VisualStudio.Threading.SemaphoreFaultedException.SemaphoreFaultedException() -> void -Microsoft.VisualStudio.Threading.IllegalSemaphoreUsageException -Microsoft.VisualStudio.Threading.IllegalSemaphoreUsageException.IllegalSemaphoreUsageException(string! message) -> void -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.DeadlockCheckTimeout.get -> System.TimeSpan diff --git a/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Shipped.txt b/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Shipped.txt deleted file mode 100644 index d8a42137d..000000000 --- a/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Shipped.txt +++ /dev/null @@ -1,454 +0,0 @@ -#nullable enable -Microsoft.VisualStudio.Threading.AsyncAutoResetEvent -Microsoft.VisualStudio.Threading.AsyncAutoResetEvent.AsyncAutoResetEvent() -> void -Microsoft.VisualStudio.Threading.AsyncAutoResetEvent.AsyncAutoResetEvent(bool allowInliningAwaiters) -> void -Microsoft.VisualStudio.Threading.AsyncAutoResetEvent.Set() -> void -Microsoft.VisualStudio.Threading.AsyncAutoResetEvent.WaitAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncAutoResetEvent.WaitAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncBarrier -Microsoft.VisualStudio.Threading.AsyncBarrier.AsyncBarrier(int participants) -> void -Microsoft.VisualStudio.Threading.AsyncBarrier.SignalAndWait() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncCountdownEvent -Microsoft.VisualStudio.Threading.AsyncCountdownEvent.AsyncCountdownEvent(int initialCount) -> void -Microsoft.VisualStudio.Threading.AsyncCountdownEvent.Signal() -> void -Microsoft.VisualStudio.Threading.AsyncCountdownEvent.SignalAndWaitAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncCountdownEvent.SignalAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncCountdownEvent.WaitAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncEventHandler -Microsoft.VisualStudio.Threading.AsyncEventHandler -Microsoft.VisualStudio.Threading.AsyncLazy -Microsoft.VisualStudio.Threading.AsyncLazy.AsyncLazy(System.Func!>! valueFactory, Microsoft.VisualStudio.Threading.JoinableTaskFactory? joinableTaskFactory = null) -> void -Microsoft.VisualStudio.Threading.AsyncLazy.GetValue() -> T -Microsoft.VisualStudio.Threading.AsyncLazy.GetValue(System.Threading.CancellationToken cancellationToken) -> T -Microsoft.VisualStudio.Threading.AsyncLazy.GetValueAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncLazy.GetValueAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncLazy.IsValueCreated.get -> bool -Microsoft.VisualStudio.Threading.AsyncLazy.IsValueFactoryCompleted.get -> bool -Microsoft.VisualStudio.Threading.AsyncLazyInitializer -Microsoft.VisualStudio.Threading.AsyncLazyInitializer.AsyncLazyInitializer(System.Func! action, Microsoft.VisualStudio.Threading.JoinableTaskFactory? joinableTaskFactory = null) -> void -Microsoft.VisualStudio.Threading.AsyncLazyInitializer.Initialize(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void -Microsoft.VisualStudio.Threading.AsyncLazyInitializer.InitializeAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncLazyInitializer.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AsyncLazyInitializer.IsCompletedSuccessfully.get -> bool -Microsoft.VisualStudio.Threading.AsyncLocal -Microsoft.VisualStudio.Threading.AsyncLocal.AsyncLocal() -> void -Microsoft.VisualStudio.Threading.AsyncLocal.Value.get -> T? -Microsoft.VisualStudio.Threading.AsyncLocal.Value.set -> void -Microsoft.VisualStudio.Threading.AsyncManualResetEvent -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.AsyncManualResetEvent(bool initialState = false, bool allowInliningAwaiters = false) -> void -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.GetAwaiter() -> System.Runtime.CompilerServices.TaskAwaiter -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.IsSet.get -> bool -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.PulseAll() -> void -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.PulseAllAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.Reset() -> void -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.Set() -> void -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.SetAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.WaitAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncManualResetEvent.WaitAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncQueue -Microsoft.VisualStudio.Threading.AsyncQueue.AsyncQueue() -> void -Microsoft.VisualStudio.Threading.AsyncQueue.Complete() -> void -Microsoft.VisualStudio.Threading.AsyncQueue.Completion.get -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncQueue.Count.get -> int -Microsoft.VisualStudio.Threading.AsyncQueue.DequeueAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncQueue.Enqueue(T value) -> void -Microsoft.VisualStudio.Threading.AsyncQueue.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AsyncQueue.IsEmpty.get -> bool -Microsoft.VisualStudio.Threading.AsyncQueue.Peek() -> T -Microsoft.VisualStudio.Threading.AsyncQueue.SyncRoot.get -> object! -Microsoft.VisualStudio.Threading.AsyncQueue.TryDequeue(System.Predicate! valueCheck, out T value) -> bool -Microsoft.VisualStudio.Threading.AsyncQueue.TryDequeue(out T value) -> bool -Microsoft.VisualStudio.Threading.AsyncQueue.TryEnqueue(T value) -> bool -Microsoft.VisualStudio.Threading.AsyncQueue.TryPeek(out T value) -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.AmbientLock.get -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.AsyncReaderWriterLock() -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.AsyncReaderWriterLock(bool captureDiagnostics) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaiter! -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaiter -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaiter.GetResult() -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Releaser -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaiter.UnsafeOnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.CaptureDiagnostics.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.CaptureDiagnostics.set -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Complete() -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Completion.get -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Dispose() -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.GetAggregateLockFlags() -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.HideLocks() -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Suppression -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsAnyLockHeld.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsAnyPassiveLockHeld.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsPassiveReadLockHeld.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsPassiveUpgradeableReadLockHeld.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsPassiveWriteLockHeld.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsReadLockHeld.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsUpgradeableReadLockHeld.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsWriteLockHeld.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags.None = 0 -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags.StickyWrite = 1 -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.Data.get -> object? -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.Data.set -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.Flags.get -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.HasReadLock.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.HasUpgradeableReadLock.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.HasWriteLock.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.IsActive.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.IsReadLock.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.IsUpgradeableReadLock.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.IsValid.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.IsWriteLock.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle.NestingLock.get -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockStackContains(Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags flags, Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle handle) -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.OnBeforeWriteLockReleased(System.Func! action) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.OnCriticalFailure(string! message) -> System.Exception! -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.ReadLockAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Releaser -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Releaser.Dispose() -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Releaser.ReleaseAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Suppression -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Suppression.Dispose() -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.SyncObject.get -> object! -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.UpgradeableReadLockAsync(Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.UpgradeableReadLockAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.WriteLockAsync(Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockFlags options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.WriteLockAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Awaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.AsyncReaderWriterResourceLock() -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.AsyncReaderWriterResourceLock(bool captureDiagnostics) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.GetAggregateLockFlags() -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags.None = 0 -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags.SkipInitialPreparation = 4096 -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags.StickyWrite = 1 -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ReadLockAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaiter -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaiter -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaiter.GetResult() -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceReleaser -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceReleaser -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceReleaser.Dispose() -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceReleaser.GetResourceAsync(TMoniker resourceMoniker, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceReleaser.ReleaseAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.SetAllResourcesToUnknownState() -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.SetResourceAsAccessed(System.Func! resourceCheck, object? state) -> bool -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.SetResourceAsAccessed(TResource! resource) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.UpgradeableReadLockAsync(Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.UpgradeableReadLockAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.WriteLockAsync(Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaitable -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.WriteLockAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceAwaitable -Microsoft.VisualStudio.Threading.AsyncSemaphore -Microsoft.VisualStudio.Threading.AsyncSemaphore.AsyncSemaphore(int initialCount) -> void -Microsoft.VisualStudio.Threading.AsyncSemaphore.CurrentCount.get -> int -Microsoft.VisualStudio.Threading.AsyncSemaphore.Dispose() -> void -Microsoft.VisualStudio.Threading.AsyncSemaphore.EnterAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncSemaphore.EnterAsync(System.TimeSpan timeout, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncSemaphore.EnterAsync(int timeout, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.AsyncSemaphore.Releaser -Microsoft.VisualStudio.Threading.AsyncSemaphore.Releaser.Dispose() -> void -Microsoft.VisualStudio.Threading.AwaitExtensions -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaitable -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaitable.ConfiguredTaskYieldAwaitable(bool continueOnCapturedContext) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaiter.ConfiguredTaskYieldAwaiter(bool continueOnCapturedContext) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaiter.GetResult() -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable.ExecuteContinuationSynchronouslyAwaitable(System.Threading.Tasks.Task! antecedent) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable.ExecuteContinuationSynchronouslyAwaitable(System.Threading.Tasks.Task! antecedent) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter.ExecuteContinuationSynchronouslyAwaiter(System.Threading.Tasks.Task! antecedent) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter.GetResult() -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter.ExecuteContinuationSynchronouslyAwaiter(System.Threading.Tasks.Task! antecedent) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter.GetResult() -> T -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaitable -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaitable.TaskSchedulerAwaitable(System.Threading.Tasks.TaskScheduler! taskScheduler, bool alwaysYield = false) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaiter.GetResult() -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaiter.TaskSchedulerAwaiter(System.Threading.Tasks.TaskScheduler! scheduler, bool alwaysYield = false) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.CancellationTokenExtensions -Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken -Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.CombinedCancellationToken(System.Threading.CancellationToken cancellationToken) -> void -Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.CombinedCancellationToken(System.Threading.CancellationTokenSource! cancellationTokenSource) -> void -Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.Dispose() -> void -Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.Equals(Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken other) -> bool -Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.Token.get -> System.Threading.CancellationToken -Microsoft.VisualStudio.Threading.DelegatingJoinableTaskFactory -Microsoft.VisualStudio.Threading.DelegatingJoinableTaskFactory.DelegatingJoinableTaskFactory(Microsoft.VisualStudio.Threading.JoinableTaskFactory! innerFactory) -> void -Microsoft.VisualStudio.Threading.HangReportContribution -Microsoft.VisualStudio.Threading.HangReportContribution.Content.get -> string! -Microsoft.VisualStudio.Threading.HangReportContribution.ContentName.get -> string? -Microsoft.VisualStudio.Threading.HangReportContribution.ContentType.get -> string? -Microsoft.VisualStudio.Threading.HangReportContribution.HangReportContribution(string! content, string? contentType, string? contentName) -> void -Microsoft.VisualStudio.Threading.HangReportContribution.HangReportContribution(string! content, string? contentType, string? contentName, params Microsoft.VisualStudio.Threading.HangReportContribution![]? nestedReports) -> void -Microsoft.VisualStudio.Threading.HangReportContribution.NestedReports.get -> System.Collections.Generic.IReadOnlyCollection? -Microsoft.VisualStudio.Threading.IAsyncDisposable -Microsoft.VisualStudio.Threading.IAsyncDisposable.DisposeAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.IHangReportContributor -Microsoft.VisualStudio.Threading.IHangReportContributor.GetHangReport() -> Microsoft.VisualStudio.Threading.HangReportContribution! -Microsoft.VisualStudio.Threading.JoinableTask -Microsoft.VisualStudio.Threading.JoinableTask.GetAwaiter() -> System.Runtime.CompilerServices.TaskAwaiter -Microsoft.VisualStudio.Threading.JoinableTask.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.JoinableTask.Join(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void -Microsoft.VisualStudio.Threading.JoinableTask.JoinAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.JoinableTask.Task.get -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.JoinableTask -Microsoft.VisualStudio.Threading.JoinableTask.GetAwaiter() -> System.Runtime.CompilerServices.TaskAwaiter -Microsoft.VisualStudio.Threading.JoinableTask.Join(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> T -Microsoft.VisualStudio.Threading.JoinableTask.JoinAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.JoinableTask.Task.get -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.JoinableTaskCollection -Microsoft.VisualStudio.Threading.JoinableTaskCollection.Add(Microsoft.VisualStudio.Threading.JoinableTask! joinableTask) -> void -Microsoft.VisualStudio.Threading.JoinableTaskCollection.Contains(Microsoft.VisualStudio.Threading.JoinableTask! joinableTask) -> bool -Microsoft.VisualStudio.Threading.JoinableTaskCollection.Context.get -> Microsoft.VisualStudio.Threading.JoinableTaskContext! -Microsoft.VisualStudio.Threading.JoinableTaskCollection.DisplayName.get -> string? -Microsoft.VisualStudio.Threading.JoinableTaskCollection.DisplayName.set -> void -Microsoft.VisualStudio.Threading.JoinableTaskCollection.GetEnumerator() -> System.Collections.Generic.IEnumerator! -Microsoft.VisualStudio.Threading.JoinableTaskCollection.Join() -> Microsoft.VisualStudio.Threading.JoinableTaskCollection.JoinRelease -Microsoft.VisualStudio.Threading.JoinableTaskCollection.JoinRelease -Microsoft.VisualStudio.Threading.JoinableTaskCollection.JoinRelease.Dispose() -> void -Microsoft.VisualStudio.Threading.JoinableTaskCollection.JoinTillEmptyAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.JoinableTaskCollection.JoinTillEmptyAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.JoinableTaskCollection.JoinableTaskCollection(Microsoft.VisualStudio.Threading.JoinableTaskContext! context, bool refCountAddedJobs = false) -> void -Microsoft.VisualStudio.Threading.JoinableTaskCollection.Remove(Microsoft.VisualStudio.Threading.JoinableTask! joinableTask) -> void -Microsoft.VisualStudio.Threading.JoinableTaskContext -Microsoft.VisualStudio.Threading.JoinableTaskContext.CreateCollection() -> Microsoft.VisualStudio.Threading.JoinableTaskCollection! -Microsoft.VisualStudio.Threading.JoinableTaskContext.Dispose() -> void -Microsoft.VisualStudio.Threading.JoinableTaskContext.Factory.get -> Microsoft.VisualStudio.Threading.JoinableTaskFactory! -Microsoft.VisualStudio.Threading.JoinableTaskContext.HangDetails -Microsoft.VisualStudio.Threading.JoinableTaskContext.HangDetails.EntryMethod.get -> System.Reflection.MethodInfo? -Microsoft.VisualStudio.Threading.JoinableTaskContext.HangDetails.HangDetails(System.TimeSpan hangDuration, int notificationCount, System.Guid hangId, System.Reflection.MethodInfo? entryMethod) -> void -Microsoft.VisualStudio.Threading.JoinableTaskContext.HangDetails.HangDuration.get -> System.TimeSpan -Microsoft.VisualStudio.Threading.JoinableTaskContext.HangDetails.HangId.get -> System.Guid -Microsoft.VisualStudio.Threading.JoinableTaskContext.HangDetails.NotificationCount.get -> int -Microsoft.VisualStudio.Threading.JoinableTaskContext.IsMainThreadBlocked() -> bool -Microsoft.VisualStudio.Threading.JoinableTaskContext.IsOnMainThread.get -> bool -Microsoft.VisualStudio.Threading.JoinableTaskContext.IsWithinJoinableTask.get -> bool -Microsoft.VisualStudio.Threading.JoinableTaskContext.JoinableTaskContext() -> void -Microsoft.VisualStudio.Threading.JoinableTaskContext.JoinableTaskContext(System.Threading.Thread? mainThread = null, System.Threading.SynchronizationContext? synchronizationContext = null) -> void -Microsoft.VisualStudio.Threading.JoinableTaskContext.MainThread.get -> System.Threading.Thread! -Microsoft.VisualStudio.Threading.JoinableTaskContext.RevertRelevance -Microsoft.VisualStudio.Threading.JoinableTaskContext.RevertRelevance.Dispose() -> void -Microsoft.VisualStudio.Threading.JoinableTaskContext.SuppressRelevance() -> Microsoft.VisualStudio.Threading.JoinableTaskContext.RevertRelevance -Microsoft.VisualStudio.Threading.JoinableTaskContextException -Microsoft.VisualStudio.Threading.JoinableTaskContextException.JoinableTaskContextException() -> void -Microsoft.VisualStudio.Threading.JoinableTaskContextException.JoinableTaskContextException(System.Runtime.Serialization.SerializationInfo! info, System.Runtime.Serialization.StreamingContext context) -> void -Microsoft.VisualStudio.Threading.JoinableTaskContextException.JoinableTaskContextException(string? message) -> void -Microsoft.VisualStudio.Threading.JoinableTaskContextException.JoinableTaskContextException(string? message, System.Exception? inner) -> void -Microsoft.VisualStudio.Threading.JoinableTaskContextNode -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.Context.get -> Microsoft.VisualStudio.Threading.JoinableTaskContext! -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.CreateCollection() -> Microsoft.VisualStudio.Threading.JoinableTaskCollection! -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.Factory.get -> Microsoft.VisualStudio.Threading.JoinableTaskFactory! -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.IsMainThreadBlocked() -> bool -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.IsOnMainThread.get -> bool -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.JoinableTaskContextNode(Microsoft.VisualStudio.Threading.JoinableTaskContext! context) -> void -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.MainThread.get -> System.Threading.Thread! -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.RegisterOnHangDetected() -> System.IDisposable! -Microsoft.VisualStudio.Threading.JoinableTaskContextNode.SuppressRelevance() -> Microsoft.VisualStudio.Threading.JoinableTaskContext.RevertRelevance -Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions -Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions.LongRunning = 1 -> Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions -Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions.None = 0 -> Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions -Microsoft.VisualStudio.Threading.JoinableTaskFactory -Microsoft.VisualStudio.Threading.JoinableTaskFactory.Add(Microsoft.VisualStudio.Threading.JoinableTask! joinable) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.Context.get -> Microsoft.VisualStudio.Threading.JoinableTaskContext! -Microsoft.VisualStudio.Threading.JoinableTaskFactory.HangDetectionTimeout.get -> System.TimeSpan -Microsoft.VisualStudio.Threading.JoinableTaskFactory.HangDetectionTimeout.set -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.IsWaitingOnLongRunningTask() -> bool -Microsoft.VisualStudio.Threading.JoinableTaskFactory.JoinableTaskFactory(Microsoft.VisualStudio.Threading.JoinableTaskCollection! collection) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.JoinableTaskFactory(Microsoft.VisualStudio.Threading.JoinableTaskContext! owner) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable -Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaiter -Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaiter -Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaiter.GetResult() -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.Run(System.Func! asyncMethod) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.Run(System.Func! asyncMethod, Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions creationOptions) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.Run(System.Func!>! asyncMethod) -> T -Microsoft.VisualStudio.Threading.JoinableTaskFactory.Run(System.Func!>! asyncMethod, Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions creationOptions) -> T -Microsoft.VisualStudio.Threading.JoinableTaskFactory.RunAsync(System.Func! asyncMethod) -> Microsoft.VisualStudio.Threading.JoinableTask! -Microsoft.VisualStudio.Threading.JoinableTaskFactory.RunAsync(System.Func! asyncMethod, Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions creationOptions) -> Microsoft.VisualStudio.Threading.JoinableTask! -Microsoft.VisualStudio.Threading.JoinableTaskFactory.RunAsync(System.Func!>! asyncMethod) -> Microsoft.VisualStudio.Threading.JoinableTask! -Microsoft.VisualStudio.Threading.JoinableTaskFactory.RunAsync(System.Func!>! asyncMethod, Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions creationOptions) -> Microsoft.VisualStudio.Threading.JoinableTask! -Microsoft.VisualStudio.Threading.JoinableTaskFactory.SwitchToMainThreadAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable -Microsoft.VisualStudio.Threading.JoinableTaskFactory.SwitchToMainThreadAsync(bool alwaysYield, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable -Microsoft.VisualStudio.Threading.JoinableTaskFactory.UnderlyingSynchronizationContext.get -> System.Threading.SynchronizationContext? -Microsoft.VisualStudio.Threading.NoMessagePumpSyncContext -Microsoft.VisualStudio.Threading.NoMessagePumpSyncContext.NoMessagePumpSyncContext() -> void -Microsoft.VisualStudio.Threading.ProgressWithCompletion -Microsoft.VisualStudio.Threading.ProgressWithCompletion.ProgressWithCompletion(System.Action! handler) -> void -Microsoft.VisualStudio.Threading.ProgressWithCompletion.ProgressWithCompletion(System.Action! handler, Microsoft.VisualStudio.Threading.JoinableTaskFactory? joinableTaskFactory) -> void -Microsoft.VisualStudio.Threading.ProgressWithCompletion.ProgressWithCompletion(System.Func! handler) -> void -Microsoft.VisualStudio.Threading.ProgressWithCompletion.ProgressWithCompletion(System.Func! handler, Microsoft.VisualStudio.Threading.JoinableTaskFactory? joinableTaskFactory) -> void -Microsoft.VisualStudio.Threading.ProgressWithCompletion.WaitAsync() -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.ProgressWithCompletion.WaitAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.VisualStudio.Threading.ReentrantSemaphore -Microsoft.VisualStudio.Threading.ReentrantSemaphore.CurrentCount.get -> int -Microsoft.VisualStudio.Threading.ReentrantSemaphore.Dispose() -> void -Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode -Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode.Freeform = 3 -> Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode -Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode.NotAllowed = 0 -> Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode -Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode.NotRecognized = 1 -> Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode -Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode.Stack = 2 -> Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode -Microsoft.VisualStudio.Threading.ReentrantSemaphore.RevertRelevance -Microsoft.VisualStudio.Threading.ReentrantSemaphore.RevertRelevance.Dispose() -> void -Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters -Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters.Attributes = 2 -> Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters -Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters.Security = 8 -> Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters -Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters.Subkey = 1 -> Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters -Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters.Value = 4 -> Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters -Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext -Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.Frame -Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.Frame.Continue.get -> bool -Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.Frame.Continue.set -> void -Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.Frame.Frame() -> void -Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.PushFrame(Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.Frame! frame) -> void -Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.SingleThreadedSynchronizationContext() -> void -Microsoft.VisualStudio.Threading.SpecializedSyncContext -Microsoft.VisualStudio.Threading.SpecializedSyncContext.Dispose() -> void -Microsoft.VisualStudio.Threading.ThreadingTools -Microsoft.VisualStudio.Threading.TplExtensions -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaitable -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaiter -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaitable.NoThrowTaskAwaitable(System.Threading.Tasks.Task! task, bool captureContext) -> void -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaiter -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaiter.GetResult() -> void -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaiter.NoThrowTaskAwaiter(System.Threading.Tasks.Task! task, bool captureContext) -> void -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void -abstract Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.GetResourceAsync(TMoniker resourceMoniker, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -abstract Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.PrepareResourceForConcurrentAccessAsync(TResource! resource, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -abstract Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.PrepareResourceForExclusiveAccessAsync(TResource! resource, Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.LockFlags lockFlags, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -abstract Microsoft.VisualStudio.Threading.ReentrantSemaphore.ExecuteAsync(System.Func! operation, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -abstract Microsoft.VisualStudio.Threading.ReentrantSemaphore.ExecuteAsync(System.Func>! operation, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -override Microsoft.VisualStudio.Threading.AsyncLazy.ToString() -> string! -override Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.OnExclusiveLockReleasedAsync() -> System.Threading.Tasks.Task! -override Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.OnUpgradeableReadLockReleased() -> void -override Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.Equals(object? obj) -> bool -override Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.GetHashCode() -> int -override Microsoft.VisualStudio.Threading.DelegatingJoinableTaskFactory.OnTransitionedToMainThread(Microsoft.VisualStudio.Threading.JoinableTask! joinableTask, bool canceled) -> void -override Microsoft.VisualStudio.Threading.DelegatingJoinableTaskFactory.OnTransitioningToMainThread(Microsoft.VisualStudio.Threading.JoinableTask! joinableTask) -> void -override Microsoft.VisualStudio.Threading.DelegatingJoinableTaskFactory.PostToUnderlyingSynchronizationContext(System.Threading.SendOrPostCallback! callback, object! state) -> void -override Microsoft.VisualStudio.Threading.DelegatingJoinableTaskFactory.WaitSynchronously(System.Threading.Tasks.Task! task) -> void -override Microsoft.VisualStudio.Threading.NoMessagePumpSyncContext.Wait(System.IntPtr[]! waitHandles, bool waitAll, int millisecondsTimeout) -> int -override Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.CreateCopy() -> System.Threading.SynchronizationContext! -override Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.Post(System.Threading.SendOrPostCallback! d, object? state) -> void -override Microsoft.VisualStudio.Threading.SingleThreadedSynchronizationContext.Send(System.Threading.SendOrPostCallback! d, object? state) -> void -static Microsoft.VisualStudio.Threading.AwaitExtensions.ConfigureAwait(this System.Runtime.CompilerServices.YieldAwaitable yieldAwaitable, bool continueOnCapturedContext) -> Microsoft.VisualStudio.Threading.AwaitExtensions.ConfiguredTaskYieldAwaitable -static Microsoft.VisualStudio.Threading.AwaitExtensions.ConfigureAwaitRunInline(this System.Threading.Tasks.Task! antecedent) -> Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable -static Microsoft.VisualStudio.Threading.AwaitExtensions.ConfigureAwaitRunInline(this System.Threading.Tasks.Task! antecedent) -> Microsoft.VisualStudio.Threading.AwaitExtensions.ExecuteContinuationSynchronouslyAwaitable -static Microsoft.VisualStudio.Threading.AwaitExtensions.GetAwaiter(this System.Threading.Tasks.TaskScheduler! scheduler) -> Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaiter -static Microsoft.VisualStudio.Threading.AwaitExtensions.GetAwaiter(this System.Threading.WaitHandle! handle) -> System.Runtime.CompilerServices.TaskAwaiter -static Microsoft.VisualStudio.Threading.AwaitExtensions.SwitchTo(this System.Threading.Tasks.TaskScheduler! scheduler, bool alwaysYield = false) -> Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaitable -static Microsoft.VisualStudio.Threading.AwaitExtensions.WaitForChangeAsync(this Microsoft.Win32.RegistryKey! registryKey, bool watchSubtree = true, Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters change = Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters.Subkey | Microsoft.VisualStudio.Threading.RegistryChangeNotificationFilters.Value, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.AwaitExtensions.WaitForExitAsync(this System.Diagnostics.Process! process, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombineWith(this System.Threading.CancellationToken original, System.Threading.CancellationToken other) -> Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken -static Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombineWith(this System.Threading.CancellationToken original, params System.Threading.CancellationToken[]! others) -> Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken -static Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.operator !=(Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken left, Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken right) -> bool -static Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken.operator ==(Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken left, Microsoft.VisualStudio.Threading.CancellationTokenExtensions.CombinedCancellationToken right) -> bool -static Microsoft.VisualStudio.Threading.NoMessagePumpSyncContext.Default.get -> System.Threading.SynchronizationContext! -static Microsoft.VisualStudio.Threading.ReentrantSemaphore.Create(int initialCount = 1, Microsoft.VisualStudio.Threading.JoinableTaskContext? joinableTaskContext = null, Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode mode = Microsoft.VisualStudio.Threading.ReentrantSemaphore.ReentrancyMode.NotAllowed) -> Microsoft.VisualStudio.Threading.ReentrantSemaphore! -static Microsoft.VisualStudio.Threading.SpecializedSyncContext.Apply(System.Threading.SynchronizationContext? syncContext, bool checkForChangesOnRevert = true) -> Microsoft.VisualStudio.Threading.SpecializedSyncContext -static Microsoft.VisualStudio.Threading.ThreadingTools.Apply(this System.Threading.SynchronizationContext? syncContext, bool checkForChangesOnRevert = true) -> Microsoft.VisualStudio.Threading.SpecializedSyncContext -static Microsoft.VisualStudio.Threading.ThreadingTools.ApplyChangeOptimistically(ref T hotLocation, TArg applyChangeArgument, System.Func! applyChange) -> bool -static Microsoft.VisualStudio.Threading.ThreadingTools.ApplyChangeOptimistically(ref T hotLocation, System.Func! applyChange) -> bool -static Microsoft.VisualStudio.Threading.ThreadingTools.WithCancellation(this System.Threading.Tasks.Task! task, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.ThreadingTools.WithCancellation(this System.Threading.Tasks.Task! task, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.AppendAction(this System.Threading.Tasks.Task! task, System.Action! action, System.Threading.Tasks.TaskContinuationOptions options = System.Threading.Tasks.TaskContinuationOptions.None, System.Threading.CancellationToken cancellation = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.ApplyResultTo(this System.Threading.Tasks.Task! task, System.Threading.Tasks.TaskCompletionSource! tcs) -> void -static Microsoft.VisualStudio.Threading.TplExtensions.ApplyResultTo(this System.Threading.Tasks.Task! task, System.Threading.Tasks.TaskCompletionSource! tcs) -> void -static Microsoft.VisualStudio.Threading.TplExtensions.AttachToParent(this System.Threading.Tasks.Task! task) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.AttachToParent(this System.Threading.Tasks.Task! task) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.FollowCancelableTaskToCompletion(System.Func!>! taskToFollow, System.Threading.CancellationToken ultimateCancellation, System.Threading.Tasks.TaskCompletionSource? taskThatFollows = null) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.Forget(this System.Threading.Tasks.Task? task) -> void -static Microsoft.VisualStudio.Threading.TplExtensions.InvokeAsync(this Microsoft.VisualStudio.Threading.AsyncEventHandler? handlers, object? sender, System.EventArgs! args) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.InvokeAsync(this Microsoft.VisualStudio.Threading.AsyncEventHandler? handlers, object? sender, TEventArgs args) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.NoThrowAwaitable(this System.Threading.Tasks.Task! task, bool captureContext = true) -> Microsoft.VisualStudio.Threading.TplExtensions.NoThrowTaskAwaitable -static Microsoft.VisualStudio.Threading.TplExtensions.ToApm(this System.Threading.Tasks.Task! task, System.AsyncCallback? callback, object? state) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.ToApm(this System.Threading.Tasks.Task! task, System.AsyncCallback? callback, object? state) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.ToTask(this System.Threading.WaitHandle! handle, int timeout = -1, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.WaitWithoutInlining(this System.Threading.Tasks.Task! task) -> void -static Microsoft.VisualStudio.Threading.TplExtensions.WithTimeout(this System.Threading.Tasks.Task! task, System.TimeSpan timeout) -> System.Threading.Tasks.Task! -static Microsoft.VisualStudio.Threading.TplExtensions.WithTimeout(this System.Threading.Tasks.Task! task, System.TimeSpan timeout) -> System.Threading.Tasks.Task! -static readonly Microsoft.VisualStudio.Threading.TplExtensions.CanceledTask -> System.Threading.Tasks.Task! -static readonly Microsoft.VisualStudio.Threading.TplExtensions.CompletedTask -> System.Threading.Tasks.Task! -static readonly Microsoft.VisualStudio.Threading.TplExtensions.FalseTask -> System.Threading.Tasks.Task! -static readonly Microsoft.VisualStudio.Threading.TplExtensions.TrueTask -> System.Threading.Tasks.Task! -virtual Microsoft.VisualStudio.Threading.AsyncQueue.InitialCapacity.get -> int -virtual Microsoft.VisualStudio.Threading.AsyncQueue.OnCompleted() -> void -virtual Microsoft.VisualStudio.Threading.AsyncQueue.OnDequeued(T value) -> void -virtual Microsoft.VisualStudio.Threading.AsyncQueue.OnEnqueued(T value, bool alreadyDispatched) -> void -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.CanCurrentThreadHoldActiveLock.get -> bool -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Dispose(bool disposing) -> void -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.GetHangReport() -> Microsoft.VisualStudio.Threading.HangReportContribution! -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.GetTaskSchedulerForReadLockRequest() -> System.Threading.Tasks.TaskScheduler! -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.IsUnsupportedSynchronizationContext.get -> bool -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.NoMessagePumpSynchronizationContext.get -> System.Threading.SynchronizationContext! -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.OnBeforeExclusiveLockReleasedAsync() -> System.Threading.Tasks.Task! -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.OnBeforeLockReleasedAsync(bool exclusiveLockRelease, Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.LockHandle releasingLock) -> System.Threading.Tasks.Task! -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.OnCriticalFailure(System.Exception! ex) -> System.Exception! -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.OnExclusiveLockReleasedAsync() -> System.Threading.Tasks.Task! -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.OnUpgradeableReadLockReleased() -> void -virtual Microsoft.VisualStudio.Threading.AsyncSemaphore.Dispose(bool disposing) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskContext.CreateDefaultFactory() -> Microsoft.VisualStudio.Threading.JoinableTaskFactory! -virtual Microsoft.VisualStudio.Threading.JoinableTaskContext.CreateFactory(Microsoft.VisualStudio.Threading.JoinableTaskCollection! collection) -> Microsoft.VisualStudio.Threading.JoinableTaskFactory! -virtual Microsoft.VisualStudio.Threading.JoinableTaskContext.Dispose(bool disposing) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskContext.GetHangReport() -> Microsoft.VisualStudio.Threading.HangReportContribution! -virtual Microsoft.VisualStudio.Threading.JoinableTaskContext.NoMessagePumpSynchronizationContext.get -> System.Threading.SynchronizationContext! -virtual Microsoft.VisualStudio.Threading.JoinableTaskContext.OnFalseHangDetected(System.TimeSpan hangDuration, System.Guid hangId) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskContext.OnHangDetected(System.TimeSpan hangDuration, int notificationCount, System.Guid hangId) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskContextNode.CreateDefaultFactory() -> Microsoft.VisualStudio.Threading.JoinableTaskFactory! -virtual Microsoft.VisualStudio.Threading.JoinableTaskContextNode.CreateFactory(Microsoft.VisualStudio.Threading.JoinableTaskCollection! collection) -> Microsoft.VisualStudio.Threading.JoinableTaskFactory! -virtual Microsoft.VisualStudio.Threading.JoinableTaskContextNode.OnFalseHangDetected(System.TimeSpan hangDuration, System.Guid hangId) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskContextNode.OnHangDetected(Microsoft.VisualStudio.Threading.JoinableTaskContext.HangDetails! details) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskContextNode.OnHangDetected(System.TimeSpan hangDuration, int notificationCount, System.Guid hangId) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskFactory.OnTransitionedToMainThread(Microsoft.VisualStudio.Threading.JoinableTask! joinableTask, bool canceled) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskFactory.OnTransitioningToMainThread(Microsoft.VisualStudio.Threading.JoinableTask! joinableTask) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskFactory.PostToUnderlyingSynchronizationContext(System.Threading.SendOrPostCallback! callback, object! state) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskFactory.WaitSynchronously(System.Threading.Tasks.Task! task) -> void -virtual Microsoft.VisualStudio.Threading.JoinableTaskFactory.WaitSynchronouslyCore(System.Threading.Tasks.Task! task) -> void -virtual Microsoft.VisualStudio.Threading.ProgressWithCompletion.Report(T value) -> void -virtual Microsoft.VisualStudio.Threading.ReentrantSemaphore.SuppressRelevance() -> Microsoft.VisualStudio.Threading.ReentrantSemaphore.RevertRelevance -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.Releaser.DisposeAsync() -> System.Threading.Tasks.ValueTask -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.ResourceReleaser.DisposeAsync() -> System.Threading.Tasks.ValueTask -Microsoft.VisualStudio.Threading.NonConcurrentSynchronizationContext -Microsoft.VisualStudio.Threading.NonConcurrentSynchronizationContext.NonConcurrentSynchronizationContext(bool sticky) -> void -Microsoft.VisualStudio.Threading.NonConcurrentSynchronizationContext.UnhandledException -> System.EventHandler? -override Microsoft.VisualStudio.Threading.NonConcurrentSynchronizationContext.CreateCopy() -> System.Threading.SynchronizationContext! -override Microsoft.VisualStudio.Threading.NonConcurrentSynchronizationContext.Post(System.Threading.SendOrPostCallback! d, object? state) -> void -override Microsoft.VisualStudio.Threading.NonConcurrentSynchronizationContext.Send(System.Threading.SendOrPostCallback! d, object? state) -> void -static Microsoft.VisualStudio.Threading.TplExtensions.Forget(this System.Threading.Tasks.ValueTask task) -> void -static Microsoft.VisualStudio.Threading.TplExtensions.Forget(this System.Threading.Tasks.ValueTask task) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaitable -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaitable.AggregateExceptionAwaitable(System.Threading.Tasks.Task! task, bool continueOnCapturedContext) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaiter -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaiter.AggregateExceptionAwaiter(System.Threading.Tasks.Task! task, bool continueOnCapturedContext) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaiter.GetResult() -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void -static Microsoft.VisualStudio.Threading.AwaitExtensions.ConfigureAwaitForAggregateException(this System.Threading.Tasks.Task! task, bool continueOnCapturedContext = true) -> Microsoft.VisualStudio.Threading.AwaitExtensions.AggregateExceptionAwaitable diff --git a/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt deleted file mode 100644 index e85d3c718..000000000 --- a/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,8 +0,0 @@ -Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.AsyncReaderWriterLock(Microsoft.VisualStudio.Threading.JoinableTaskContext? joinableTaskContext, bool captureDiagnostics = false) -> void -Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.AsyncReaderWriterResourceLock(Microsoft.VisualStudio.Threading.JoinableTaskContext? joinableTaskContext, bool captureDiagnostics) -> void -Microsoft.VisualStudio.Threading.JoinableTaskContext.IsMainThreadMaybeBlocked() -> bool -Microsoft.VisualStudio.Threading.SemaphoreFaultedException -Microsoft.VisualStudio.Threading.SemaphoreFaultedException.SemaphoreFaultedException() -> void -Microsoft.VisualStudio.Threading.IllegalSemaphoreUsageException -Microsoft.VisualStudio.Threading.IllegalSemaphoreUsageException.IllegalSemaphoreUsageException(string! message) -> void -virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterLock.DeadlockCheckTimeout.get -> System.TimeSpan diff --git a/src/Microsoft.VisualStudio.Threading/OptProf.targets b/src/OptProf.targets similarity index 67% rename from src/Microsoft.VisualStudio.Threading/OptProf.targets rename to src/OptProf.targets index 51f826ad0..11809c0de 100644 --- a/src/Microsoft.VisualStudio.Threading/OptProf.targets +++ b/src/OptProf.targets @@ -5,10 +5,8 @@ Common7\IDE\PublicAssemblies\Microsoft.VisualStudio.Threading.17.x\$(TargetFileName) /ExeConfig:"%VisualStudio.InstallationUnderTest.Path%\Common7\IDE\vsn.exe" - - + - diff --git a/src/SosThreadingTools/Commands.cs b/src/SosThreadingTools/Commands.cs index 3013737e3..2decbd170 100644 --- a/src/SosThreadingTools/Commands.cs +++ b/src/SosThreadingTools/Commands.cs @@ -1,52 +1,31 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace CpsDbg -{ - using System; - using System.Collections.Generic; - using System.Runtime.InteropServices; +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; - internal static class Commands - { - private const string DumpAsyncCommand = "dumpasync"; +namespace CpsDbg; - private static readonly Dictionary CommandHandlers = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { "dumpasync", new DumpAsyncCommand() }, - }; +internal static class Commands +{ + [UnmanagedCallersOnly(EntryPoint = "dumpasync", CallConvs = new[] { typeof(CallConvStdcall) })] + public static unsafe void DumpAsync(IntPtr client, byte* args) + { + ExecuteCommand(new DumpAsyncCommand(client, isRunningAsExtension: true), args); + } - [DllExport(DumpAsyncCommand, CallingConvention.StdCall)] - internal static void DumpAsync(IntPtr client, [MarshalAs(UnmanagedType.LPStr)] string args) + private static unsafe void ExecuteCommand(ICommandHandler command, byte* args) + { + try { - ExecuteCommand(client, DumpAsyncCommand, args); + string? strArgs = Marshal.PtrToStringAnsi((IntPtr)args); + command.Execute(strArgs ?? string.Empty); } - - private static void ExecuteCommand(IntPtr client, string command, [MarshalAs(UnmanagedType.LPStr)] string args) + catch (Exception ex) { - ICommandHandler handler; - if (!CommandHandlers.TryGetValue(command, out handler)) - { - return; - } - - DebuggerContext? context = DebuggerContext.GetDebuggerContext(client); - if (context is null) - { - return; - } - - try - { - handler.Execute(context, args); - } -#pragma warning disable CA1031 // Do not catch general exception types - catch (Exception ex) -#pragma warning restore CA1031 // Do not catch general exception types - { - context.Output.WriteLine($"Encountered an unhandled exception running '{command}':"); - context.Output.WriteLine(ex.ToString()); - } + Console.WriteLine($"Encountered an unhandled exception running '{command}':"); + Console.WriteLine(ex.ToString()); } } } diff --git a/src/SosThreadingTools/DebuggerContext.cs b/src/SosThreadingTools/DebuggerContext.cs deleted file mode 100644 index 493e3d0e6..000000000 --- a/src/SosThreadingTools/DebuggerContext.cs +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace CpsDbg -{ - using System; - using System.Diagnostics; - using System.Globalization; - using System.IO; - using System.Linq; - using System.Reflection; - using System.Runtime.InteropServices; - using Microsoft.Diagnostics.Runtime; - using Microsoft.Diagnostics.Runtime.Interop; - - internal class DebuggerContext - { - private const string ClrMD = "Microsoft.Diagnostics.Runtime"; - - /// - /// The singleton instance used in a debug session. - /// - private static DebuggerContext? instance; - - static DebuggerContext() - { - AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly; - } - - private DebuggerContext(IDebugClient debugClient, DataTarget dataTarget, ClrRuntime runtime, DebuggerOutput output) - { - this.DebugClient = debugClient; - this.DataTarget = dataTarget; - this.Runtime = runtime; - this.Output = output; - } - - internal ClrRuntime Runtime { get; } - - internal DebuggerOutput Output { get; } - - internal IDebugClient DebugClient { get; } - - internal IDebugControl DebugControl => (IDebugControl)this.DebugClient; - - private DataTarget DataTarget { get; } - - internal static DebuggerContext? GetDebuggerContext(IntPtr ptrClient) - { - // On our first call to the API: - // 1. Store a copy of IDebugClient in DebugClient. - // 2. Replace Console's output stream to be the debugger window. - // 3. Create an instance of DataTarget using the IDebugClient. - if (instance is null) - { - object client = Marshal.GetUniqueObjectForIUnknown(ptrClient); - var debugClient = (IDebugClient)client; - - var output = new DebuggerOutput(debugClient); - -#pragma warning disable CA2000 // Dispose objects before losing scope - var dataTarget = DataTarget.CreateFromDbgEng(ptrClient); -#pragma warning restore CA2000 // Dispose objects before losing scope - - ClrRuntime? runtime = null; - - // If our ClrRuntime instance is null, it means that this is our first call, or - // that the dac wasn't loaded on any previous call. Find the dac loaded in the - // process (the user must use .cordll), then construct our runtime from it. - - // Just find a module named mscordacwks and assume it's the one the user - // loaded into windbg. - Process p = Process.GetCurrentProcess(); - foreach (ProcessModule module in p.Modules) - { - if (module.FileName.ToUpperInvariant().Contains("MSCORDACWKS")) - { - // TODO: This does not support side-by-side CLRs. - runtime = dataTarget.ClrVersions.Single().CreateRuntime(module.FileName); - break; - } - } - - // Otherwise, the user didn't run .cordll. - if (runtime is null) - { - output.WriteLine("Mscordacwks.dll not loaded into the debugger."); - output.WriteLine("Run .cordll to load the dac before running this command."); - } - - if (runtime is object) - { - instance = new DebuggerContext(debugClient, dataTarget, runtime, output); - } - } - else - { - // If we already had a runtime, flush it for this use. This is ONLY required - // for a live process or iDNA trace. If you use the IDebug* apis to detect - // that we are debugging a crash dump you may skip this call for better perf. - // instance.Runtime.Flush(); - } - - return instance; - } - - private static Assembly? ResolveAssembly(object sender, ResolveEventArgs args) - { - if (args.Name.Contains(ClrMD)) - { - string codebase = Assembly.GetExecutingAssembly().CodeBase; - - if (codebase.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) - { - codebase = codebase.Substring(8).Replace('/', '\\'); - } - - string directory = Path.GetDirectoryName(codebase); - string path = Path.Combine(directory, ClrMD) + ".dll"; - return Assembly.LoadFile(path); - } - - return null; - } - } -} diff --git a/src/SosThreadingTools/DebuggerOutput.cs b/src/SosThreadingTools/DebuggerOutput.cs deleted file mode 100644 index dcda4ca60..000000000 --- a/src/SosThreadingTools/DebuggerOutput.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace CpsDbg -{ - using Microsoft.Diagnostics.Runtime.Interop; - - internal class DebuggerOutput - { - private IDebugClient client; - private IDebugControl control; - - internal DebuggerOutput(IDebugClient client) - { - this.client = client; - this.control = (IDebugControl)client; - } - - internal void WriteString(string message) - { - this.control.ControlledOutput(DEBUG_OUTCTL.ALL_CLIENTS, DEBUG_OUTPUT.NORMAL, message); - } - - internal void WriteLine(string message) - { - this.WriteString(message + "\n"); - } - - internal void WriteObjectAddress(ulong address) - { - this.WriteDml($"{address:x8}"); - } - - internal void WriteThreadLink(uint threadId) - { - this.WriteDml($"Thread TID:[{threadId:x}]"); - } - - internal void WriteMethodInfo(string name, ulong address) - { - this.WriteDml($"{name}"); - } - - internal void WriteStringWithLink(string message, string linkCommand) - { - this.WriteDml($"{message}"); - } - - private void WriteDml(string dml) - { - this.control.ControlledOutput(DEBUG_OUTCTL.AMBIENT_DML, DEBUG_OUTPUT.NORMAL, dml); - } - } -} diff --git a/src/SosThreadingTools/DumpAsyncCommand.cs b/src/SosThreadingTools/DumpAsyncCommand.cs index f16aeaf69..88c73a216 100644 --- a/src/SosThreadingTools/DumpAsyncCommand.cs +++ b/src/SosThreadingTools/DumpAsyncCommand.cs @@ -1,303 +1,423 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace CpsDbg +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Runtime.Utilities.DbgEng; + +namespace CpsDbg; + +internal class DumpAsyncCommand : SOSLinkedCommand, ICommandHandler { - using System; - using System.Collections.Generic; - using System.Linq; - using Microsoft.Diagnostics.Runtime; - using Microsoft.Diagnostics.Runtime.Interop; + internal DumpAsyncCommand(IntPtr pUnknown, bool isRunningAsExtension) + : base(pUnknown, isRunningAsExtension) + { + } - internal class DumpAsyncCommand : ICommandHandler + internal DumpAsyncCommand(IDisposable dbgEng, bool isRunningAsExtension = false) + : base(dbgEng, isRunningAsExtension) { - public void Execute(DebuggerContext context, string args) + } + + public void Execute(string args) + { + // There can be multiple CLR runtimes loaded into + foreach (ClrRuntime runtime in this.Runtimes) { - ClrHeap heap = context.Runtime.Heap; + ClrHeap heap = runtime.Heap; var allStateMachines = new List(); var knownStateMachines = new Dictionary(); - GetAllStateMachines(context, heap, allStateMachines, knownStateMachines); + GetAllStateMachines(heap, allStateMachines, knownStateMachines); - ChainStateMachinesBasedOnTaskContinuations(context, knownStateMachines); - ChainStateMachinesBasedOnJointableTasks(context, allStateMachines); - MarkThreadingBlockTasks(context, allStateMachines); + ChainStateMachinesBasedOnTaskContinuations(knownStateMachines); + ChainStateMachinesBasedOnJointableTasks(allStateMachines); + this.MarkThreadingBlockTasks(heap, allStateMachines); MarkUIThreadDependingTasks(allStateMachines); FixBrokenDependencies(allStateMachines); - PrintOutStateMachines(allStateMachines, context.Output); - LoadCodePages(context, allStateMachines); + this.PrintOutStateMachines(allStateMachines); + this.LoadCodePages(allStateMachines); } + } - private static void GetAllStateMachines(DebuggerContext context, ClrHeap heap, List allStateMachines, Dictionary knownStateMachines) + private static void GetAllStateMachines(ClrHeap heap, List allStateMachines, Dictionary knownStateMachines) + { + foreach (ClrObject obj in heap.GetObjectsOfType("System.Runtime.CompilerServices.AsyncMethodBuilderCore+MoveNextRunner")) { - foreach (ClrObject obj in heap.GetObjectsOfType("System.Runtime.CompilerServices.AsyncMethodBuilderCore+MoveNextRunner")) + try { - try + ClrObject stateMachine = obj.ReadObjectField("m_stateMachine"); + if (!knownStateMachines.ContainsKey(stateMachine.Address)) { - ClrObject stateMachine = obj.ReadObjectField("m_stateMachine"); - if (!knownStateMachines.ContainsKey(stateMachine.Address)) + try { - try + var state = stateMachine.ReadField("<>1__state"); + if (state >= -1) { - var state = stateMachine.ReadField("<>1__state"); - if (state >= -1) + ClrObject taskField = default(ClrObject); + ClrValueType? asyncBuilder = stateMachine.TryGetValueClassField("<>t__builder"); + if (asyncBuilder.HasValue) { - ClrObject taskField = default(ClrObject); - ClrValueType? asyncBuilder = stateMachine.TryGetValueClassField("<>t__builder"); - if (asyncBuilder.HasValue) + while (asyncBuilder.HasValue) { - while (asyncBuilder.HasValue) + taskField = asyncBuilder.TryGetObjectField("m_task"); + if (!taskField.IsNull) { - taskField = asyncBuilder.TryGetObjectField("m_task"); - if (!taskField.IsNull) - { - break; - } + break; + } - ClrValueType? nextAsyncBuilder = asyncBuilder.TryGetValueClassField("m_builder"); - if (nextAsyncBuilder is null) - { - asyncBuilder = asyncBuilder.TryGetValueClassField("_methodBuilder"); - } - else - { - asyncBuilder = nextAsyncBuilder; - } + ClrValueType? nextAsyncBuilder = asyncBuilder.TryGetValueClassField("m_builder"); + if (nextAsyncBuilder is null) + { + asyncBuilder = asyncBuilder.TryGetValueClassField("_methodBuilder"); + } + else + { + asyncBuilder = nextAsyncBuilder; } } - else + } + else + { + // CLR debugger may not be able to access t__builder, when NGEN assemblies are being used, and the type of the field could be lost. + // Our workaround is to pick up the first Task object referenced by the state machine, which seems to be correct. + // That function works with the raw data structure (like how GC scans the object, so it doesn't depend on symbols. + // + // However, one problem of that is we can pick up tasks from other reference fields of the same structure. So, we go through fields which we have symbols + // and remember references encounted, and we skip them when we go through GC references. + // Note: we can do better by going through other value structures, and extract references from them here, which we can consider when we have a real scenario. + var previousReferences = new Dictionary(); + if (stateMachine.Type?.GetFieldByName("<>t__builder") is not null) { - // CLR debugger may not be able to access t__builder, when NGEN assemblies are being used, and the type of the field could be lost. - // Our workaround is to pick up the first Task object referenced by the state machine, which seems to be correct. - // That function works with the raw data structure (like how GC scans the object, so it doesn't depend on symbols. - // - // However, one problem of that is we can pick up tasks from other reference fields of the same structure. So, we go through fields which we have symbols - // and remember references encounted, and we skip them when we go through GC references. - // Note: we can do better by going through other value structures, and extract references from them here, which we can consider when we have a real scenario. - var previousReferences = new Dictionary(); - if (stateMachine.Type?.GetFieldByName("<>t__builder") is not null) + foreach (ClrInstanceField field in stateMachine.Type.Fields) { - foreach (ClrInstanceField field in stateMachine.Type.Fields) + if (string.Equals(field.Name, "<>t__builder", StringComparison.Ordinal)) { - if (string.Equals(field.Name, "<>t__builder", StringComparison.Ordinal)) - { - break; - } + break; + } - if (field.IsObjectReference) + if (field.IsObjectReference) + { + ClrObject referencedValue = field.ReadObject(stateMachine.Address, interior: false); + if (!referencedValue.IsNull) { - ClrObject referencedValue = field.ReadObject(stateMachine.Address, interior: false); - if (!referencedValue.IsNull) + if (previousReferences.TryGetValue(referencedValue.Address, out int refCount)) { - if (previousReferences.TryGetValue(referencedValue.Address, out int refCount)) - { - previousReferences[referencedValue.Address] = refCount + 1; - } - else - { - previousReferences[referencedValue.Address] = 1; - } + previousReferences[referencedValue.Address] = refCount + 1; + } + else + { + previousReferences[referencedValue.Address] = 1; } } } } + } - foreach (ClrObject referencedObject in stateMachine.EnumerateReferences(true)) + foreach (ClrObject referencedObject in stateMachine.EnumerateReferences(true)) + { + if (!referencedObject.IsNull) { - if (!referencedObject.IsNull) + if (previousReferences.TryGetValue(referencedObject.Address, out int refCount) && refCount > 0) { - if (previousReferences.TryGetValue(referencedObject.Address, out int refCount) && refCount > 0) + if (refCount == 1) { - if (refCount == 1) - { - previousReferences.Remove(referencedObject.Address); - } - else - { - previousReferences[referencedObject.Address] = refCount - 1; - } - - continue; + previousReferences.Remove(referencedObject.Address); } - else if (previousReferences.Count > 0) + else { - continue; + previousReferences[referencedObject.Address] = refCount - 1; } - if (referencedObject.Type is object && - (string.Equals(referencedObject.Type.Name, "System.Threading.Tasks.Task", StringComparison.Ordinal) || string.Equals(referencedObject.Type.BaseType?.Name, "System.Threading.Tasks.Task", StringComparison.Ordinal))) - { - taskField = referencedObject; - break; - } + continue; + } + else if (previousReferences.Count > 0) + { + continue; + } + + if (referencedObject.Type is object && + (string.Equals(referencedObject.Type.Name, "System.Threading.Tasks.Task", StringComparison.Ordinal) || string.Equals(referencedObject.Type.BaseType?.Name, "System.Threading.Tasks.Task", StringComparison.Ordinal))) + { + taskField = referencedObject; + break; } } } + } - var asyncState = new AsyncStateMachine(state, stateMachine, taskField); - allStateMachines.Add(asyncState); - knownStateMachines.Add(stateMachine.Address, asyncState); + var asyncState = new AsyncStateMachine(state, stateMachine, taskField); + allStateMachines.Add(asyncState); + knownStateMachines.Add(stateMachine.Address, asyncState); - if (stateMachine.Type is object) + if (stateMachine.Type is object) + { + foreach (ClrMethod? method in stateMachine.Type.Methods) { - foreach (ClrMethod? method in stateMachine.Type.Methods) + if (method.Name == "MoveNext" && method.NativeCode != ulong.MaxValue) { - if (method.Name == "MoveNext" && method.NativeCode != ulong.MaxValue) - { - asyncState.CodeAddress = method.NativeCode; - } + asyncState.CodeAddress = method.NativeCode; } } } + + ClrObject currentObject = stateMachine.TryGetObjectField("<>4__this"); + if (!currentObject.IsNull && currentObject.Type is not null && string.Equals(currentObject.Type.Name, "Microsoft.VisualStudio.Threading.JoinableTaskCollection", StringComparison.Ordinal)) + { + asyncState.WaitingJoinableTasks = GetJoinableTasksFromCollection(currentObject); + } } + } #pragma warning disable CA1031 // Do not catch general exception types - catch (Exception ex) + catch (Exception ex) #pragma warning restore CA1031 // Do not catch general exception types - { - context.Output.WriteLine($"Fail to process state machine {stateMachine.Address:x} Type:'{stateMachine.Type?.Name}' Module:'{stateMachine.Type?.Module?.Name}' Error: {ex.Message}"); - } + { + Console.WriteLine($"Fail to process state machine {stateMachine.Address:x} Type:'{stateMachine.Type?.Name}' Module:'{stateMachine.Type?.Module?.Name}' Error: {ex.Message}"); } } + } #pragma warning disable CA1031 // Do not catch general exception types - catch (Exception ex) + catch (Exception ex) #pragma warning restore CA1031 // Do not catch general exception types - { - context.Output.WriteLine($"Fail to process AsyncStateMachine Runner {obj.Address:x} Error: {ex.Message}"); - } + { + Console.WriteLine($"Fail to process AsyncStateMachine Runner {obj.Address:x} Error: {ex.Message}"); } } + } - private static void ChainStateMachinesBasedOnTaskContinuations(DebuggerContext context, Dictionary knownStateMachines) + private static List GetJoinableTasksFromCollection(ClrObject joinableTaskCollection) + { + var joinableTasks = new List(); + ClrValueType? dependentData = joinableTaskCollection.TryGetValueClassField("dependentData"); + if (dependentData is not null) { - foreach (AsyncStateMachine? stateMachine in knownStateMachines.Values) + ClrObject childDependentNodes = dependentData.TryGetObjectField("childDependentNodes"); + if (!childDependentNodes.IsNull && + childDependentNodes.TryReadField("count", out int count) && + childDependentNodes.TryReadField("freeCount", out int freeCount)) { - ClrObject taskObject = stateMachine.Task; - try + count -= freeCount; + if (count > 0) { - while (!taskObject.IsNull) + ClrObject entries = childDependentNodes.TryGetObjectField("entries"); + if (!entries.IsNull && entries.IsArray && entries.AsArray() is ClrArray entriesArray) { - // 3 cases in order to get the _target: - // 1. m_continuationObject.m_action._target - // 2. m_continuationObject._target - // 3. m_continuationObject.m_task.m_stateObject._target - ClrObject continuationObject = taskObject.TryGetObjectField("m_continuationObject"); - if (continuationObject.IsNull) + for (int i = 0; i < entriesArray.Length; i++) { - break; + ClrValueType? value = entriesArray.GetStructValue(i); + ClrObject key = value.TryGetObjectField("key"); + if (!key.IsNull) + { + joinableTasks.Add(key); + if (--count == 0) + { + break; + } + } } + } + } + } + } - ChainStateMachineBasedOnTaskContinuations(knownStateMachines, stateMachine, continuationObject); + return joinableTasks; + } - taskObject = continuationObject; + private static void ChainStateMachinesBasedOnTaskContinuations(Dictionary knownStateMachines) + { + foreach (AsyncStateMachine? stateMachine in knownStateMachines.Values) + { + ClrObject taskObject = stateMachine.Task; + try + { + while (!taskObject.IsNull) + { + // 3 cases in order to get the _target: + // 1. m_continuationObject.m_action._target + // 2. m_continuationObject._target + // 3. m_continuationObject.m_task.m_stateObject._target + ClrObject continuationObject = taskObject.TryGetObjectField("m_continuationObject"); + if (continuationObject.IsNull) + { + break; } + + ChainStateMachineBasedOnTaskContinuations(knownStateMachines, stateMachine, continuationObject); + + taskObject = continuationObject; } + } #pragma warning disable CA1031 // Do not catch general exception types - catch (Exception ex) + catch (Exception ex) #pragma warning restore CA1031 // Do not catch general exception types - { - context.Output.WriteLine($"Fail to fix continuation of state {stateMachine.StateMachine.Address:x} Error: {ex.Message}"); - } + { + Console.WriteLine($"Fail to fix continuation of state {stateMachine.StateMachine.Address:x} Error: {ex.Message}"); } } + } - private static void ChainStateMachineBasedOnTaskContinuations(Dictionary knownStateMachines, AsyncStateMachine stateMachine, ClrObject continuationObject) - { - ClrObject continuationAction = continuationObject.TryGetObjectField("m_action"); + private static void ChainStateMachineBasedOnTaskContinuations(Dictionary knownStateMachines, AsyncStateMachine stateMachine, ClrObject continuationObject) + { + ClrObject continuationAction = continuationObject.TryGetObjectField("m_action"); - // case 1 - ClrObject continuationTarget = continuationAction.TryGetObjectField("_target"); + // case 1 + ClrObject continuationTarget = continuationAction.TryGetObjectField("_target"); + if (continuationTarget.IsNull) + { + // case 2 + continuationTarget = continuationObject.TryGetObjectField("_target"); if (continuationTarget.IsNull) { - // case 2 - continuationTarget = continuationObject.TryGetObjectField("_target"); - if (continuationTarget.IsNull) + // case 3 + continuationTarget = continuationObject.TryGetObjectField("m_task").TryGetObjectField("m_stateObject").TryGetObjectField("_target"); + } + } + + while (!continuationTarget.IsNull) + { + // now get the continuation from the target + ClrObject continuationTargetStateMachine = continuationTarget.TryGetObjectField("m_stateMachine"); + if (!continuationTargetStateMachine.IsNull) + { + if (knownStateMachines.TryGetValue(continuationTargetStateMachine.Address, out AsyncStateMachine? targetAsyncState) && targetAsyncState != stateMachine) { - // case 3 - continuationTarget = continuationObject.TryGetObjectField("m_task").TryGetObjectField("m_stateObject").TryGetObjectField("_target"); + stateMachine.Next = targetAsyncState; + stateMachine.DependentCount++; + targetAsyncState.Previous = stateMachine; } + + break; } + else + { + ClrObject nextContinuation = continuationTarget.TryGetObjectField("m_continuation"); + continuationTarget = nextContinuation.TryGetObjectField("_target"); + } + } - while (!continuationTarget.IsNull) + ClrObject items = continuationObject.TryGetObjectField("_items"); + if (!items.IsNull && items.IsArray && items.ContainsPointers) + { + foreach (ClrObject promise in items.EnumerateReferences(true)) { - // now get the continuation from the target - ClrObject continuationTargetStateMachine = continuationTarget.TryGetObjectField("m_stateMachine"); - if (!continuationTargetStateMachine.IsNull) + if (!promise.IsNull) { - AsyncStateMachine targetAsyncState; - if (knownStateMachines.TryGetValue(continuationTargetStateMachine.Address, out targetAsyncState) && targetAsyncState != stateMachine) + ClrObject innerContinuationObject = promise.TryGetObjectField("m_continuationObject"); + if (!innerContinuationObject.IsNull) { - stateMachine.Next = targetAsyncState; - stateMachine.DependentCount++; - targetAsyncState.Previous = stateMachine; + ChainStateMachineBasedOnTaskContinuations(knownStateMachines, stateMachine, innerContinuationObject); } + else + { + ChainStateMachineBasedOnTaskContinuations(knownStateMachines, stateMachine, promise); + } + } + } + } + } - break; + private static void ChainStateMachinesBasedOnJointableTasks(List allStateMachines) + { + foreach (AsyncStateMachine? stateMachine in allStateMachines) + { + if (stateMachine.Previous is null) + { + try + { + ClrObject joinableTask = stateMachine.StateMachine.TryGetObjectField("<>4__this"); + FindWaitingTaskFromJoinableTask(joinableTask, stateMachine); + + if (stateMachine.WaitingJoinableTasks is List joinableTasks) + { + foreach (ClrObject waitingJoinableTask in joinableTasks) + { + FindWaitingTaskFromJoinableTask(waitingJoinableTask, stateMachine); + } + } } - else +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types { - ClrObject nextContinuation = continuationTarget.TryGetObjectField("m_continuation"); - continuationTarget = nextContinuation.TryGetObjectField("_target"); + Console.WriteLine($"Fail to fix continuation of state {stateMachine.StateMachine.Address:x} Error: {ex.Message}"); } } + } - ClrObject items = continuationObject.TryGetObjectField("_items"); - if (!items.IsNull && items.IsArray && items.ContainsPointers) + void FindWaitingTaskFromJoinableTask(ClrObject joinableTask, AsyncStateMachine currentStateMachine) + { + ClrObject wrappedTask = joinableTask.TryGetObjectField("wrappedTask"); + if (!wrappedTask.IsNull) { - foreach (ClrObject promise in items.EnumerateReferences(true)) + AsyncStateMachine? previousStateMachine = allStateMachines + .FirstOrDefault(s => s.Task.Address == wrappedTask.Address); + if (previousStateMachine is object && currentStateMachine != previousStateMachine) { - if (!promise.IsNull) + if (currentStateMachine.Previous is null) { - ClrObject innerContinuationObject = promise.TryGetObjectField("m_continuationObject"); - if (!innerContinuationObject.IsNull) - { - ChainStateMachineBasedOnTaskContinuations(knownStateMachines, stateMachine, innerContinuationObject); - } - else - { - ChainStateMachineBasedOnTaskContinuations(knownStateMachines, stateMachine, promise); - } + currentStateMachine.Previous = previousStateMachine; + previousStateMachine.Next = currentStateMachine; + } + else + { + previousStateMachine.Next ??= currentStateMachine; } + + previousStateMachine.DependentCount++; } } } + } - private static void ChainStateMachinesBasedOnJointableTasks(DebuggerContext context, List allStateMachines) + private static void MarkUIThreadDependingTasks(List allStateMachines) + { + foreach (AsyncStateMachine? stateMachine in allStateMachines) { - foreach (AsyncStateMachine? stateMachine in allStateMachines) + if (stateMachine.Previous is null && stateMachine.State >= 0) { - if (stateMachine.Previous is null) + try { - try + ClrInstanceField? awaitField = stateMachine.StateMachine.Type?.GetFieldByName($"<>u__{stateMachine.State + 1}"); + if (awaitField is object && awaitField.IsValueType && string.Equals(awaitField.Type?.Name, "Microsoft.VisualStudio.Threading.JoinableTaskFactory+MainThreadAwaiter", StringComparison.Ordinal)) { - ClrObject joinableTask = stateMachine.StateMachine.TryGetObjectField("<>4__this"); - ClrObject wrappedTask = joinableTask.TryGetObjectField("wrappedTask"); - if (!wrappedTask.IsNull) + ClrValueType? awaitObject = stateMachine.StateMachine.TryGetValueClassField($"<>u__{stateMachine.State + 1}"); + if (awaitObject.HasValue) { - AsyncStateMachine? previousStateMachine = allStateMachines - .FirstOrDefault(s => s.Task.Address == wrappedTask.Address); - if (previousStateMachine is object && stateMachine != previousStateMachine) - { - stateMachine.Previous = previousStateMachine; - previousStateMachine.Next = stateMachine; - previousStateMachine.DependentCount++; - } + stateMachine.SwitchToMainThreadTask = awaitObject.TryGetObjectField("job"); } } + } #pragma warning disable CA1031 // Do not catch general exception types - catch (Exception ex) + catch (Exception) #pragma warning restore CA1031 // Do not catch general exception types - { - context.Output.WriteLine($"Fail to fix continuation of state {stateMachine.StateMachine.Address:x} Error: {ex.Message}"); - } + { } } } + } - private static void MarkThreadingBlockTasks(DebuggerContext context, List allStateMachines) + private static void FixBrokenDependencies(List allStateMachines) + { + foreach (AsyncStateMachine? stateMachine in allStateMachines) { - foreach (ClrThread? thread in context.Runtime.Threads) + if (stateMachine.Previous is object && stateMachine.Previous.Next != stateMachine) + { + // If the previous task actually has two continuations, we end up in a one way dependencies chain, we need fix it in the future. + stateMachine.AlterPrevious = stateMachine.Previous; + stateMachine.Previous = null; + } + } + } + + private void MarkThreadingBlockTasks(ClrHeap heap, List allStateMachines) + { + foreach (ClrRuntime runtime in this.Runtimes) + { + foreach (ClrThread? thread in runtime.Threads) { ClrStackFrame? stackFrame = thread.EnumerateStackTrace().Take(50).FirstOrDefault( f => f.Method is { } method @@ -307,15 +427,16 @@ private static void MarkThreadingBlockTasks(DebuggerContext context, List(); - foreach (IClrStackRoot stackRoot in thread.EnumerateStackRoots()) + foreach (ClrStackRoot stackRoot in thread.EnumerateStackRoots()) { ClrObject stackObject = stackRoot.Object; - if (string.Equals(stackObject.Type?.Name, "Microsoft.VisualStudio.Threading.JoinableTask", StringComparison.Ordinal) || - string.Equals(stackObject.Type?.BaseType?.Name, "Microsoft.VisualStudio.Threading.JoinableTask", StringComparison.Ordinal)) + if (stackObject.Type is not null && + (string.Equals(stackObject.Type.Name, "Microsoft.VisualStudio.Threading.JoinableTask", StringComparison.Ordinal) || + string.Equals(stackObject.Type.BaseType?.Name, "Microsoft.VisualStudio.Threading.JoinableTask", StringComparison.Ordinal))) { if (visitedObjects.Add(stackObject.Address)) { - var joinableTaskObject = new ClrObject(stackObject.Address, stackObject.Type); + ClrObject joinableTaskObject = heap.GetObject(stackObject.Address, stackObject.Type); int state = joinableTaskObject.ReadField("state"); if ((state & 0x10) == 0x10) { @@ -332,7 +453,7 @@ private static void MarkThreadingBlockTasks(DebuggerContext context, List allStateMachines) + private void PrintOutStateMachines(List allStateMachines) + { + int loopMark = -1; + foreach (AsyncStateMachine? stateMachine in allStateMachines) { - foreach (AsyncStateMachine? stateMachine in allStateMachines) + int depth = 0; + if (stateMachine.Previous is null) { - if (stateMachine.Previous is null && stateMachine.State >= 0) + AsyncStateMachine? p = stateMachine; + while (p is object) { - try - { - ClrInstanceField? awaitField = stateMachine.StateMachine.Type?.GetFieldByName($"<>u__{stateMachine.State + 1}"); - if (awaitField is object && awaitField.IsValueType && string.Equals(awaitField.Type?.Name, "Microsoft.VisualStudio.Threading.JoinableTaskFactory+MainThreadAwaiter", StringComparison.Ordinal)) - { - ClrValueType? awaitObject = stateMachine.StateMachine.TryGetValueClassField($"<>u__{stateMachine.State + 1}"); - if (awaitObject.HasValue) - { - stateMachine.SwitchToMainThreadTask = awaitObject.TryGetObjectField("job"); - } - } - } -#pragma warning disable CA1031 // Do not catch general exception types - catch (Exception) -#pragma warning restore CA1031 // Do not catch general exception types + depth++; + if (p.Depth == loopMark) { + break; } + + p.Depth = loopMark; + p = p.Next; } } - } - private static void FixBrokenDependencies(List allStateMachines) - { - foreach (AsyncStateMachine? stateMachine in allStateMachines) + if (stateMachine.AlterPrevious is object) { - if (stateMachine.Previous is object && stateMachine.Previous.Next != stateMachine) - { - // If the previous task actually has two continuations, we end up in a one way dependencies chain, we need fix it in the future. - stateMachine.AlterPrevious = stateMachine.Previous; - stateMachine.Previous = null; - } + depth++; } + + stateMachine.Depth = depth; + loopMark--; } - private static void PrintOutStateMachines(List allStateMachines, DebuggerOutput output) + var printedMachines = new HashSet(); + + foreach (AsyncStateMachine? node in allStateMachines + .Where(m => m.Depth > 0) + .OrderByDescending(m => m.Depth) + .ThenByDescending(m => m.SwitchToMainThreadTask.Address)) { - int loopMark = -1; - foreach (AsyncStateMachine? stateMachine in allStateMachines) - { - int depth = 0; - if (stateMachine.Previous is null) - { - AsyncStateMachine? p = stateMachine; - while (p is object) - { - depth++; - if (p.Depth == loopMark) - { - break; - } + this.PrintAsyncStateMachineChain(node, printedMachines); - p.Depth = loopMark; - p = p.Next; - } - } + Console.WriteLine(string.Empty); + } - if (stateMachine.AlterPrevious is object) + // Print nodes which we didn't print because of loops. + if (allStateMachines.Count > printedMachines.Count) + { + Console.WriteLine("States form dependencies loop -- could be an error caused by the analysis tool"); + foreach (AsyncStateMachine? node in allStateMachines) + { + if (!printedMachines.Contains(node)) { - depth++; + this.PrintAsyncStateMachineChain(node, printedMachines); + Console.WriteLine(string.Empty); } - - stateMachine.Depth = depth; - loopMark--; } + } + } - var printedMachines = new HashSet(); + private void PrintAsyncStateMachineChain(AsyncStateMachine node, HashSet printedMachines) + { + int nLevel = 0; - foreach (AsyncStateMachine? node in allStateMachines - .Where(m => m.Depth > 0) - .OrderByDescending(m => m.Depth) - .ThenByDescending(m => m.SwitchToMainThreadTask.Address)) - { - bool multipleLineBlock = PrintAsyncStateMachineChain(output, node, printedMachines); + var loopDetection = new HashSet(); + for (AsyncStateMachine? p = node; p is object; p = p.Next) + { + printedMachines.Add(p); - if (multipleLineBlock) - { - output.WriteLine(string.Empty); - } + if (nLevel > 0) + { + this.WriteString(".."); } - - // Print nodes which we didn't print because of loops. - if (allStateMachines.Count > printedMachines.Count) + else if (p.AlterPrevious is object) { - output.WriteLine("States form dependencies loop -- could be an error caused by the analysis tool"); - foreach (AsyncStateMachine? node in allStateMachines) - { - if (!printedMachines.Contains(node)) - { - PrintAsyncStateMachineChain(output, node, printedMachines); - output.WriteLine(string.Empty); - } - } + this.WriteObjectAddress(p.AlterPrevious.StateMachine.Address); + this.WriteString($" <{p.AlterPrevious.State}> * {p.AlterPrevious.StateMachine.Type?.Name} @ "); + this.WriteMethodInfo($"{p.AlterPrevious.CodeAddress:x}", p.AlterPrevious.CodeAddress); + this.WriteLine(string.Empty); + this.WriteString(".."); + } + else if (!p.SwitchToMainThreadTask.IsNull) + { + this.WriteObjectAddress(p.SwitchToMainThreadTask.Address); + this.WriteLine(".SwitchToMainThreadAsync"); + this.WriteString(".."); } - } - private static bool PrintAsyncStateMachineChain(DebuggerOutput output, AsyncStateMachine node, HashSet printedMachines) - { - int nLevel = 0; - bool multipleLineBlock = false; + this.WriteObjectAddress(p.StateMachine.Address); + string doubleDependentTaskMark = p.DependentCount > 1 ? " * " : " "; + this.WriteString($" <{p.State}>{doubleDependentTaskMark}{p.StateMachine.Type?.Name} @ "); + this.WriteMethodInfo($"{p.CodeAddress:x}", p.CodeAddress); + this.WriteLine(string.Empty); - var loopDetection = new HashSet(); - for (AsyncStateMachine? p = node; p is object; p = p.Next) + if (!loopDetection.Add(p)) { - printedMachines.Add(p); - - if (nLevel > 0) - { - output.WriteString(".."); - multipleLineBlock = true; - } - else if (p.AlterPrevious is object) - { - output.WriteObjectAddress(p.AlterPrevious.StateMachine.Address); - output.WriteString($" <{p.AlterPrevious.State}> * {p.AlterPrevious.StateMachine.Type?.Name} @ "); - output.WriteMethodInfo($"{p.AlterPrevious.CodeAddress:x}", p.AlterPrevious.CodeAddress); - output.WriteLine(string.Empty); - output.WriteString(".."); - multipleLineBlock = true; - } - else if (!p.SwitchToMainThreadTask.IsNull) - { - output.WriteObjectAddress(p.SwitchToMainThreadTask.Address); - output.WriteLine(".SwitchToMainThreadAsync"); - output.WriteString(".."); - multipleLineBlock = true; - } + this.WriteLine("!!Loop task dependencies"); + break; + } - output.WriteObjectAddress(p.StateMachine.Address); - string doubleDependentTaskMark = p.DependentCount > 1 ? " * " : " "; - output.WriteString($" <{p.State}>{doubleDependentTaskMark}{p.StateMachine.Type?.Name} @ "); - output.WriteMethodInfo($"{p.CodeAddress:x}", p.CodeAddress); - output.WriteLine(string.Empty); + if (p.Next is null && p.BlockedThread.HasValue) + { + this.WriteString("-- "); + this.WriteThreadLink(p.BlockedThread.Value); + this.WriteString(" - JoinableTask: "); + this.WriteObjectAddress(p.BlockedJoinableTask.Address); - if (!loopDetection.Add(p)) + int state = p.BlockedJoinableTask.ReadField("state"); + if ((state & 0x20) == 0x20) { - output.WriteLine("!!Loop task dependencies"); - break; + this.WriteLine(" SynchronouslyBlockingMainThread"); } - - if (p.Next is null && p.BlockedThread.HasValue) + else { - output.WriteString("-- "); - output.WriteThreadLink(p.BlockedThread.Value); - output.WriteString(" - JoinableTask: "); - output.WriteObjectAddress(p.BlockedJoinableTask.Address); - - int state = p.BlockedJoinableTask.ReadField("state"); - if ((state & 0x20) == 0x20) - { - output.WriteLine(" SynchronouslyBlockingMainThread"); - } - else - { - output.WriteLine(string.Empty); - } - - multipleLineBlock = true; + this.WriteLine(string.Empty); } - - nLevel++; } - return multipleLineBlock; + nLevel++; } + } - private static void LoadCodePages(DebuggerContext context, List allStateMachines) + private void LoadCodePages(List allStateMachines) + { + var loadedAddresses = new HashSet(); + foreach (AsyncStateMachine? stateMachine in allStateMachines) { - var loadedAddresses = new HashSet(); - foreach (AsyncStateMachine? stateMachine in allStateMachines) + ulong codeAddress = stateMachine.CodeAddress; + if (loadedAddresses.Add(codeAddress)) { - ulong codeAddress = stateMachine.CodeAddress; - if (loadedAddresses.Add(codeAddress)) - { - context.DebugControl.Execute(DEBUG_OUTCTL.IGNORE, $"u {codeAddress} {codeAddress}", DEBUG_EXECUTE.NOT_LOGGED); - } + this.DebugControl.Execute(DEBUG_OUTCTL.IGNORE, $"u {codeAddress} {codeAddress}", DEBUG_EXECUTE.NOT_LOGGED); } } + } - private class AsyncStateMachine + private class AsyncStateMachine + { + public AsyncStateMachine(int state, ClrObject stateMachine, ClrObject task) { - public AsyncStateMachine(int state, ClrObject stateMachine, ClrObject task) - { - this.State = state; - this.StateMachine = stateMachine; - this.Task = task; - } + this.State = state; + this.StateMachine = stateMachine; + this.Task = task; + } - public int State { get; } // -1 == currently running, 0 = still waiting on first await, 2= before the 3rd await + public int State { get; } // -1 == currently running, 0 = still waiting on first await, 2= before the 3rd await - public ClrObject StateMachine { get; } + public ClrObject StateMachine { get; } - public ClrObject Task { get; } + public ClrObject Task { get; } - public AsyncStateMachine? Previous { get; set; } + public AsyncStateMachine? Previous { get; set; } - public AsyncStateMachine? Next { get; set; } + public AsyncStateMachine? Next { get; set; } - public int DependentCount { get; set; } + public int DependentCount { get; set; } - public int Depth { get; set; } + public int Depth { get; set; } - public uint? BlockedThread { get; set; } + public uint? BlockedThread { get; set; } - public ClrObject BlockedJoinableTask { get; set; } + public ClrObject BlockedJoinableTask { get; set; } - public ClrObject SwitchToMainThreadTask { get; set; } + public ClrObject SwitchToMainThreadTask { get; set; } - public AsyncStateMachine? AlterPrevious { get; set; } + public AsyncStateMachine? AlterPrevious { get; set; } - public ulong CodeAddress { get; set; } + public List? WaitingJoinableTasks { get; set; } - public override string ToString() - { - return $"state = {this.State} Depth {this.Depth} StateMachine = {this.StateMachine} Task = {this.Task}"; - } + public ulong CodeAddress { get; set; } + + public override string ToString() + { + return $"state = {this.State} Depth {this.Depth} StateMachine = {this.StateMachine} Task = {this.Task}"; } } } diff --git a/src/SosThreadingTools/ExtensionContext.cs b/src/SosThreadingTools/ExtensionContext.cs index 8e6beb41d..897d4f641 100644 --- a/src/SosThreadingTools/ExtensionContext.cs +++ b/src/SosThreadingTools/ExtensionContext.cs @@ -1,44 +1,27 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace CpsDbg -{ - using System; - using System.IO; - using System.Reflection; - using System.Runtime.InteropServices; - - internal static class ExtensionContext - { - [DllExport(nameof(DebugExtensionInitialize), CallingConvention.StdCall)] - internal static int DebugExtensionInitialize(ref uint version, ref uint flags) - { - // Set the extension version to 1, which expects exports with this signature: - // void _stdcall function(IDebugClient *client, const char *args) - version = DEBUG_EXTENSION_VERSION(1, 0); - flags = 0; +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; - AppDomain currentDomain = AppDomain.CurrentDomain; - currentDomain.AssemblyResolve += new ResolveEventHandler(LoadFromSameFolder); - return 0; - } +namespace CpsDbg; - private static uint DEBUG_EXTENSION_VERSION(uint major, uint minor) - { - return ((major & 0xffff) << 16) | (minor & 0xffff); - } +internal static class ExtensionContext +{ + [UnmanagedCallersOnly(EntryPoint = nameof(DebugExtensionInitialize), CallConvs = new[] { typeof(CallConvStdcall) })] + public static unsafe int DebugExtensionInitialize(uint* pVersion, uint* pFlags) + { + // Set the extension version to 1, which expects exports with this signature: + // void _stdcall function(IDebugClient *client, const char *args) + *pVersion = DEBUG_EXTENSION_VERSION(1, 0); + *pFlags = 0; - private static Assembly? LoadFromSameFolder(object sender, ResolveEventArgs args) - { - string folderPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - string assemblyPath = Path.Combine(folderPath, new AssemblyName(args.Name).Name + ".dll"); - if (!File.Exists(assemblyPath)) - { - return null; - } + return 0; + } - Assembly assembly = Assembly.LoadFrom(assemblyPath); - return assembly; - } + private static uint DEBUG_EXTENSION_VERSION(uint major, uint minor) + { + return ((major & 0xffff) << 16) | (minor & 0xffff); } } diff --git a/src/SosThreadingTools/ICommandHandler.cs b/src/SosThreadingTools/ICommandHandler.cs index e976975fe..f71887702 100644 --- a/src/SosThreadingTools/ICommandHandler.cs +++ b/src/SosThreadingTools/ICommandHandler.cs @@ -1,10 +1,9 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace CpsDbg +namespace CpsDbg; + +internal interface ICommandHandler { - internal interface ICommandHandler - { - void Execute(DebuggerContext context, string args); - } + void Execute(string args); } diff --git a/src/SosThreadingTools/SOSLinkedCommand.cs b/src/SosThreadingTools/SOSLinkedCommand.cs new file mode 100644 index 000000000..14d6a334e --- /dev/null +++ b/src/SosThreadingTools/SOSLinkedCommand.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Net; +using System.Xml.Linq; +using DbgEngExtension; +using Microsoft.Diagnostics.Runtime.Utilities.DbgEng; + +namespace CpsDbg; + +internal class SOSLinkedCommand : DbgEngCommand +{ + private readonly bool isRunningAsExtension; + + protected SOSLinkedCommand(nint pUnknown, bool isRunningAsExtension) + : base(pUnknown, redirectConsoleOutput: isRunningAsExtension) + { + this.isRunningAsExtension = isRunningAsExtension; + } + + protected SOSLinkedCommand(IDisposable dbgEng, bool isRunningAsExtension) + : base(dbgEng, redirectConsoleOutput: isRunningAsExtension) + { + this.isRunningAsExtension = isRunningAsExtension; + } + + protected void WriteString(string message) + { + this.DebugControl.ControlledOutput(DEBUG_OUTCTL.ALL_CLIENTS, DEBUG_OUTPUT.NORMAL, message); + } + + protected void WriteLine(string message) + { + this.WriteString(message + "\n"); + } + + protected void WriteObjectAddress(ulong address) + { + if (this.isRunningAsExtension) + { + this.WriteDml($"{address:x8}"); + } + else + { + Console.WriteLine(address.ToString("x")); + } + } + + protected void WriteThreadLink(uint threadId) + { + if (this.isRunningAsExtension) + { + this.WriteDml($"Thread TID:[{threadId:x}]"); + } + else + { + Console.WriteLine($"Thread TID:[{threadId:x}]"); + } + } + + protected void WriteMethodInfo(string name, ulong address) + { + if (this.isRunningAsExtension) + { + this.WriteDml($"{name}"); + } + else + { + Console.WriteLine(name); + } + } + + protected void WriteStringWithLink(string message, string linkCommand) + { + if (this.isRunningAsExtension) + { + this.WriteDml($"{message}"); + } + else + { + Console.WriteLine(message); + } + } + + protected void WriteDml(string dml) + { + this.DebugControl.ControlledOutput(DEBUG_OUTCTL.AMBIENT_DML, DEBUG_OUTPUT.NORMAL, dml); + } +} diff --git a/src/SosThreadingTools/SosThreadingTools.csproj b/src/SosThreadingTools/SosThreadingTools.csproj index 3ae573c11..0881f882c 100644 --- a/src/SosThreadingTools/SosThreadingTools.csproj +++ b/src/SosThreadingTools/SosThreadingTools.csproj @@ -1,15 +1,20 @@ - + - net472 + net10.0-windows win-x86;win-x64 - true + true + true + false - $(MSBuildProjectName) - true - false + + false - - false + $(MSBuildProjectName) + $(MSBuildProjectName)Managed + $(MSBuildProjectName) + $(DnneCompilerUserFlags) /W3 /guard:cf + $(DnneLinkerUserFlags) /guard:cf + true A WinDBG extension that contains the !DumpAsync command for .NET Framework processes. @@ -23,13 +28,8 @@ false - - - - - - + + - diff --git a/src/SosThreadingTools/SosThreadingTools.targets b/src/SosThreadingTools/SosThreadingTools.targets index de0dcec75..166bb82e5 100644 --- a/src/SosThreadingTools/SosThreadingTools.targets +++ b/src/SosThreadingTools/SosThreadingTools.targets @@ -1,5 +1,5 @@ - + @@ -7,7 +7,6 @@ - @@ -24,24 +23,51 @@ $(TargetsForTfmSpecificContentInPackage);PackBuildOutputs - StampAndIncludeGalleryManifest;$(GenerateNuspecDependsOn) - + + + + + + + + + MicrosoftXmlSHA2 + + + + + + + + + + <_InnerBuildRid Include="$(RuntimeIdentifiers)" /> + + + + + - - - + <_PackRid Include="$(RuntimeIdentifiers)" /> + + + - + - + diff --git a/src/SosThreadingTools/Utilities.cs b/src/SosThreadingTools/Utilities.cs index 1391cc7da..616273029 100644 --- a/src/SosThreadingTools/Utilities.cs +++ b/src/SosThreadingTools/Utilities.cs @@ -1,75 +1,74 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace CpsDbg -{ - using System; - using System.Collections.Generic; - using System.Linq; - using Microsoft.Diagnostics.Runtime; +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Diagnostics.Runtime; + +namespace CpsDbg; - internal static class Utilities +internal static class Utilities +{ + internal static ClrObject TryGetObjectField(this ClrObject clrObject, string fieldName) { - internal static ClrObject TryGetObjectField(this ClrObject clrObject, string fieldName) + if (!clrObject.IsNull) { - if (!clrObject.IsNull) + ClrInstanceField? field = clrObject.Type?.GetFieldByName(fieldName); + if (field is object && field.IsObjectReference) { - ClrInstanceField? field = clrObject.Type?.GetFieldByName(fieldName); - if (field is object && field.IsObjectReference) - { - return field.ReadObject(clrObject.Address, interior: false); - } + return field.ReadObject(clrObject.Address, interior: false); } - - return default(ClrObject); } - internal static ClrValueType? TryGetValueClassField(this ClrObject clrObject, string fieldName) + return default(ClrObject); + } + + internal static ClrValueType? TryGetValueClassField(this ClrObject clrObject, string fieldName) + { + if (!clrObject.IsNull) { - if (!clrObject.IsNull) + ClrInstanceField? field = clrObject.Type?.GetFieldByName(fieldName); + if (field?.Type is object && field.Type.IsValueType) { - ClrInstanceField? field = clrObject.Type?.GetFieldByName(fieldName); - if (field?.Type is object && field.Type.IsValueType) - { - // System.Console.WriteLine("{0} {1:x} Field {2} {3} {4} {5}", clrObject.Type.Name, clrObject.Address, fieldName, field.Type.Name, field.Type.IsValueType, field.Type.IsRuntimeType); - return clrObject.ReadValueTypeField(fieldName); - } + // System.Console.WriteLine("{0} {1:x} Field {2} {3} {4} {5}", clrObject.Type.Name, clrObject.Address, fieldName, field.Type.Name, field.Type.IsValueType, field.Type.IsRuntimeType); + return clrObject.ReadValueTypeField(fieldName); } - - return null; } - internal static ClrObject TryGetObjectField(this ClrValueType? clrObject, string fieldName) + return null; + } + + internal static ClrObject TryGetObjectField(this ClrValueType? clrObject, string fieldName) + { + if (clrObject is object) { - if (clrObject is object) + ClrInstanceField? field = clrObject.Value.Type?.GetFieldByName(fieldName); + if (field is object && field.IsObjectReference) { - ClrInstanceField? field = clrObject.Value.Type?.GetFieldByName(fieldName); - if (field is object && field.IsObjectReference) - { - return clrObject.Value.ReadObjectField(fieldName); - } + return clrObject.Value.ReadObjectField(fieldName); } - - return default(ClrObject); } - internal static ClrValueType? TryGetValueClassField(this ClrValueType? clrObject, string fieldName) + return default(ClrObject); + } + + internal static ClrValueType? TryGetValueClassField(this ClrValueType? clrObject, string fieldName) + { + if (clrObject.HasValue) { - if (clrObject.HasValue) + ClrInstanceField? field = clrObject.Value.Type?.GetFieldByName(fieldName); + if (field is object && field.IsValueType) { - ClrInstanceField? field = clrObject.Value.Type?.GetFieldByName(fieldName); - if (field is object && field.IsValueType) - { - return clrObject.Value.ReadValueTypeField(fieldName); - } + return clrObject.Value.ReadValueTypeField(fieldName); } - - return null; } - internal static IEnumerable GetObjectsOfType(this ClrHeap heap, string typeName) - { - return heap.EnumerateObjects().Where(obj => string.Equals(obj.Type?.Name, typeName, StringComparison.Ordinal)); - } + return null; + } + + internal static IEnumerable GetObjectsOfType(this ClrHeap heap, string typeName) + { + return heap.EnumerateObjects().Where(obj => string.Equals(obj.Type?.Name, typeName, StringComparison.Ordinal)); } } diff --git a/src/dirs.proj b/src/dirs.proj new file mode 100644 index 000000000..b6fa81f93 --- /dev/null +++ b/src/dirs.proj @@ -0,0 +1,5 @@ + + + + + diff --git a/stylecop.json b/stylecop.json index 6c045a1a1..a8021aba6 100644 --- a/stylecop.json +++ b/stylecop.json @@ -2,14 +2,17 @@ "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", "settings": { "documentationRules": { - "companyName": "Microsoft", + "companyName": "Microsoft Corporation", "copyrightText": "Copyright (c) {companyName}. All rights reserved.\nLicensed under the {licenseName} license. See {licenseFile} file in the project root for full license information.", "variables": { "licenseName": "MIT", "licenseFile": "LICENSE" }, - "xmlHeader": false, - "fileNamingConvention": "metadata" + "fileNamingConvention": "metadata", + "xmlHeader": false + }, + "orderingRules": { + "usingDirectivesPlacement": "outsideNamespace" } } } diff --git a/test/.editorconfig b/test/.editorconfig index cb548f89c..04701aea8 100644 --- a/test/.editorconfig +++ b/test/.editorconfig @@ -77,3 +77,9 @@ dotnet_diagnostic.SA1604.severity = suggestion # SA1401: Fields should be private dotnet_diagnostic.SA1401.severity = silent + +# SA1133: Do not combine attributes +dotnet_diagnostic.SA1133.severity = silent + +# xUnit1051: Use TestContext.Current.CancellationToken +dotnet_diagnostic.xUnit1051.severity = suggestion diff --git a/test/AOT.props b/test/AOT.props new file mode 100644 index 000000000..27d5cbf21 --- /dev/null +++ b/test/AOT.props @@ -0,0 +1,16 @@ + + + Exe + true + true + true + $(AvailableRuntimeIdentifiers) + $(DefaultRuntimeIdentifier) + + + + + RuntimeIdentifier + + + diff --git a/test/Directory.Build.props b/test/Directory.Build.props index c10f34cb0..466be54e4 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -1,13 +1,14 @@ + - + false true - + true - + diff --git a/test/Directory.Build.targets b/test/Directory.Build.targets index 2faab3754..3758bb8c9 100644 --- a/test/Directory.Build.targets +++ b/test/Directory.Build.targets @@ -1,10 +1,14 @@ + - - cobertura - [xunit.*]* - - $(OutputPath)/ - + + + + + + + + + - + diff --git a/test/Directory.Packages.props b/test/Directory.Packages.props new file mode 100644 index 000000000..c79fbe79b --- /dev/null +++ b/test/Directory.Packages.props @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/test/IsolatedTestHost/App.config b/test/IsolatedTestHost/App.config deleted file mode 100644 index ce1411ac1..000000000 --- a/test/IsolatedTestHost/App.config +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/test/IsolatedTestHost/ExitCode.cs b/test/IsolatedTestHost/ExitCode.cs index ebad5684e..fd347daaa 100644 --- a/test/IsolatedTestHost/ExitCode.cs +++ b/test/IsolatedTestHost/ExitCode.cs @@ -1,51 +1,50 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace IsolatedTestHost +namespace IsolatedTestHost; + +/// +/// The meanings of each exit code that may be returned from this process. +/// +public enum ExitCode { /// - /// The meanings of each exit code that may be returned from this process. - /// - public enum ExitCode - { - /// - /// The test executed and passed. - /// - TestPassed = 0, - - /// - /// The test executed and failed. - /// - TestFailed, - - /// - /// The test threw SkipException. - /// - TestSkipped, - - /// - /// The test assembly could not be found. - /// - AssemblyNotFound, - - /// - /// The test class could not be found. - /// - TestClassNotFound, - - /// - /// The test method could not be found. - /// - TestMethodNotFound, - - /// - /// The test class or test method took parameters that are not supported by this host. - /// - TestNotSupported, - - /// - /// Too few or too many command line arguments passed to this process. - /// - UnexpectedCommandLineArgs, - } + /// The test executed and passed. + /// + TestPassed = 0, + + /// + /// The test executed and failed. + /// + TestFailed, + + /// + /// The test threw SkipException. + /// + TestSkipped, + + /// + /// The test assembly could not be found. + /// + AssemblyNotFound, + + /// + /// The test class could not be found. + /// + TestClassNotFound, + + /// + /// The test method could not be found. + /// + TestMethodNotFound, + + /// + /// The test class or test method took parameters that are not supported by this host. + /// + TestNotSupported, + + /// + /// Too few or too many command line arguments passed to this process. + /// + UnexpectedCommandLineArgs, } diff --git a/test/IsolatedTestHost/IsolatedTestHost.csproj b/test/IsolatedTestHost/IsolatedTestHost.csproj index a34637781..353005b37 100644 --- a/test/IsolatedTestHost/IsolatedTestHost.csproj +++ b/test/IsolatedTestHost/IsolatedTestHost.csproj @@ -3,10 +3,11 @@ Exe net472 + false - + diff --git a/test/IsolatedTestHost/Program.cs b/test/IsolatedTestHost/Program.cs index b6d9a6a44..a20655442 100644 --- a/test/IsolatedTestHost/Program.cs +++ b/test/IsolatedTestHost/Program.cs @@ -1,120 +1,117 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace IsolatedTestHost +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace IsolatedTestHost; + +internal static class Program { - using System; - using System.IO; - using System.Linq; - using System.Reflection; - using System.Threading; - using System.Threading.Tasks; - using Xunit; - - internal static class Program + private static int Main(string[] args) { - private static int Main(string[] args) + if (args.Length != 3) { - if (args.Length != 3) - { - return (int)ExitCode.UnexpectedCommandLineArgs; - } + return (int)ExitCode.UnexpectedCommandLineArgs; + } - string assemblyFile = args[0]; - string testClassName = args[1]; - string testMethodName = args[2]; + string assemblyFile = args[0]; + string testClassName = args[1]; + string testMethodName = args[2]; - return (int)MyMain(assemblyFile, testClassName, testMethodName); + return (int)MyMain(assemblyFile, testClassName, testMethodName); + } + + private static ExitCode MyMain(string assemblyFile, string testClassName, string testMethodName) + { + Assembly assembly; + try + { + assembly = Assembly.LoadFrom(assemblyFile); + } + catch (FileNotFoundException ex) + { + Console.Error.WriteLine(ex.Message); + return ExitCode.AssemblyNotFound; } - private static ExitCode MyMain(string assemblyFile, string testClassName, string testMethodName) + Type testClass = assembly.GetType(testClassName); + if (testClass is null) { - Assembly assembly; - try - { - assembly = Assembly.LoadFrom(assemblyFile); - } - catch (FileNotFoundException ex) - { - Console.Error.WriteLine(ex.Message); - return ExitCode.AssemblyNotFound; - } + return ExitCode.TestClassNotFound; + } - Type testClass = assembly.GetType(testClassName); - if (testClass is null) + MethodInfo testMethod = testClass.GetRuntimeMethod(testMethodName, Type.EmptyTypes); + if (testMethod is null) + { + return ExitCode.TestMethodNotFound; + } + + bool fact = testMethod.GetCustomAttributesData().Any(a => a.AttributeType.Name == "FactAttribute"); + if (fact) + { + return ExecuteTest(testClass, testMethod); + } + + bool stafact = testMethod.GetCustomAttributesData().Any(a => a.AttributeType.Name == "StaFactAttribute"); + if (stafact) + { + ExitCode result = ExitCode.TestFailed; + var testThread = new Thread(() => { - return ExitCode.TestClassNotFound; - } + result = ExecuteTest(testClass, testMethod); + }); + testThread.SetApartmentState(ApartmentState.STA); + testThread.Start(); + testThread.Join(); + return result; + } - MethodInfo testMethod = testClass.GetRuntimeMethod(testMethodName, Type.EmptyTypes); - if (testMethod is null) + return ExitCode.TestNotSupported; + } + + private static ExitCode ExecuteTest(Type testClass, MethodInfo testMethod) + { + try + { + ConstructorInfo? ctorWithLogger = testClass.GetConstructors().FirstOrDefault( + ctor => ctor.GetParameters().Length == 1 && ctor.GetParameters()[0].ParameterType.IsAssignableFrom(typeof(TestOutputHelper))); + ConstructorInfo? ctorDefault = testClass.GetConstructor(Type.EmptyTypes); + object? testClassInstance = + ctorWithLogger?.Invoke(new object[] { new TestOutputHelper() }) ?? + ctorDefault?.Invoke(Type.EmptyTypes); + if (testClassInstance is null) { - return ExitCode.TestMethodNotFound; + return ExitCode.TestNotSupported; } - bool fact = testMethod.GetCustomAttributesData().Any(a => a.AttributeType.Name == "FactAttribute"); - bool skippableFact = testMethod.GetCustomAttributesData().Any(a => a.AttributeType.Name == "SkippableFactAttribute"); - if (fact || skippableFact) + object result = testMethod.Invoke(testClassInstance, Type.EmptyTypes); + if (result is Task resultTask) { - return ExecuteTest(testClass, testMethod); + resultTask.GetAwaiter().GetResult(); } - bool stafact = testMethod.GetCustomAttributesData().Any(a => a.AttributeType.Name == "StaFactAttribute"); - if (stafact) + if (testClassInstance is IDisposable disposableTestClass) { - ExitCode result = ExitCode.TestFailed; - var testThread = new Thread(() => - { - result = ExecuteTest(testClass, testMethod); - }); - testThread.SetApartmentState(ApartmentState.STA); - testThread.Start(); - testThread.Join(); - return result; + disposableTestClass.Dispose(); } - return ExitCode.TestNotSupported; + return ExitCode.TestPassed; } - - private static ExitCode ExecuteTest(Type testClass, MethodInfo testMethod) + catch (Exception ex) { - try - { - ConstructorInfo? ctorWithLogger = testClass.GetConstructors().FirstOrDefault( - ctor => ctor.GetParameters().Length == 1 && ctor.GetParameters()[0].ParameterType.IsAssignableFrom(typeof(TestOutputHelper))); - ConstructorInfo? ctorDefault = testClass.GetConstructor(Type.EmptyTypes); - object? testClassInstance = - ctorWithLogger?.Invoke(new object[] { new TestOutputHelper() }) ?? - ctorDefault?.Invoke(Type.EmptyTypes); - if (testClassInstance is null) - { - return ExitCode.TestNotSupported; - } - - object result = testMethod.Invoke(testClassInstance, Type.EmptyTypes); - if (result is Task resultTask) - { - resultTask.GetAwaiter().GetResult(); - } - - if (testClassInstance is IDisposable disposableTestClass) - { - disposableTestClass.Dispose(); - } - - return ExitCode.TestPassed; - } - catch (Exception ex) + if (ex.GetType().Name == "SkipException") { - if (ex.GetType().Name == "SkipException") - { - return ExitCode.TestSkipped; - } - - Console.Error.WriteLine("Test failed."); - Console.Error.WriteLine(ex); - return ExitCode.TestFailed; + return ExitCode.TestSkipped; } + + Console.Error.WriteLine("Test failed."); + Console.Error.WriteLine(ex); + return ExitCode.TestFailed; } } } diff --git a/test/IsolatedTestHost/TestOutputHelper.cs b/test/IsolatedTestHost/TestOutputHelper.cs index dc2ab2634..f91502ede 100644 --- a/test/IsolatedTestHost/TestOutputHelper.cs +++ b/test/IsolatedTestHost/TestOutputHelper.cs @@ -1,21 +1,23 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace IsolatedTestHost +using System.Globalization; +using System.Text; +using Xunit; + +namespace IsolatedTestHost; + +internal class TestOutputHelper : ITestOutputHelper { - using System; - using Xunit.Abstractions; - - internal class TestOutputHelper : ITestOutputHelper - { - public void WriteLine(string message) - { - Console.WriteLine(message); - } - - public void WriteLine(string format, params object[] args) - { - Console.WriteLine(format, args); - } - } + private readonly StringBuilder builder = new(); + + public string Output => this.builder.ToString(); + + public void Write(string message) => this.builder.Append(message); + + public void Write(string format, params object[] args) => this.builder.AppendFormat(format, args); + + public void WriteLine(string message) => this.builder.AppendLine(message); + + public void WriteLine(string format, params object[] args) => this.builder.AppendLine(string.Format(CultureInfo.CurrentCulture, format, args)); } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/AdditionalFiles/vs-threading.SyncMethodsToExcludeFromVSTHRD103.mocks.txt b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/AdditionalFiles/vs-threading.SyncMethodsToExcludeFromVSTHRD103.mocks.txt new file mode 100644 index 000000000..491c94c55 --- /dev/null +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/AdditionalFiles/vs-threading.SyncMethodsToExcludeFromVSTHRD103.mocks.txt @@ -0,0 +1,2 @@ +# Test exclusions for VSTHRD103 analyzer +[TestNamespace.TestClass]::SlowSyncMethod \ No newline at end of file diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/App.net472.config b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/App.net472.config new file mode 100644 index 000000000..f47db784d --- /dev/null +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/App.net472.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/CommonInterestParsingTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/CommonInterestParsingTests.cs new file mode 100644 index 000000000..879c1fe53 --- /dev/null +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/CommonInterestParsingTests.cs @@ -0,0 +1,244 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.Threading.Analyzers; + +public class CommonInterestParsingTests +{ + public class TryParseNegatableTypeOrMemberReferenceTests + { + [Fact] + public void TypeOnly() + { + Assert.True(CommonInterestParsing.TryParseNegatableTypeOrMemberReference("[MyType]", out bool negated, out ReadOnlyMemory typeName, out string? memberName)); + Assert.False(negated); + Assert.Equal("MyType", typeName.ToString()); + Assert.Null(memberName); + } + + [Fact] + public void TypeWithNamespace() + { + Assert.True(CommonInterestParsing.TryParseNegatableTypeOrMemberReference("[My.Namespace.MyType]", out bool negated, out ReadOnlyMemory typeName, out string? memberName)); + Assert.False(negated); + Assert.Equal("My.Namespace.MyType", typeName.ToString()); + Assert.Null(memberName); + } + + [Fact] + public void TypeAndMember() + { + Assert.True(CommonInterestParsing.TryParseNegatableTypeOrMemberReference("[MyType]::MyMethod", out bool negated, out ReadOnlyMemory typeName, out string? memberName)); + Assert.False(negated); + Assert.Equal("MyType", typeName.ToString()); + Assert.Equal("MyMethod", memberName); + } + + [Fact] + public void TypeWithNamespaceAndMember() + { + Assert.True(CommonInterestParsing.TryParseNegatableTypeOrMemberReference("[My.Namespace.MyType]::MyMethod", out bool negated, out ReadOnlyMemory typeName, out string? memberName)); + Assert.False(negated); + Assert.Equal("My.Namespace.MyType", typeName.ToString()); + Assert.Equal("MyMethod", memberName); + } + + [Fact] + public void NegatedTypeOnly() + { + Assert.True(CommonInterestParsing.TryParseNegatableTypeOrMemberReference("![MyType]", out bool negated, out ReadOnlyMemory typeName, out string? memberName)); + Assert.True(negated); + Assert.Equal("MyType", typeName.ToString()); + Assert.Null(memberName); + } + + [Fact] + public void NegatedTypeAndMember() + { + Assert.True(CommonInterestParsing.TryParseNegatableTypeOrMemberReference("![MyType]::MyMethod", out bool negated, out ReadOnlyMemory typeName, out string? memberName)); + Assert.True(negated); + Assert.Equal("MyType", typeName.ToString()); + Assert.Equal("MyMethod", memberName); + } + + [Fact] + public void TrailingWhitespace() + { + Assert.True(CommonInterestParsing.TryParseNegatableTypeOrMemberReference("[MyType] ", out _, out ReadOnlyMemory typeName, out _)); + Assert.Equal("MyType", typeName.ToString()); + } + + [Fact] + public void TrailingWhitespaceAfterMember() + { + Assert.True(CommonInterestParsing.TryParseNegatableTypeOrMemberReference("[MyType]::MyMethod ", out _, out ReadOnlyMemory typeName, out string? memberName)); + Assert.Equal("MyType", typeName.ToString()); + Assert.Equal("MyMethod", memberName); + } + + /// + /// Regression test: the long type name from the bug report must parse quickly without catastrophic backtracking. + /// + [Fact] + public void LongQualifiedTypeNameFromBugReport() + { + Assert.True(CommonInterestParsing.TryParseNegatableTypeOrMemberReference("![Microsoft.VisualStudio.Shell.Interop.IVsRunningDocumentTablePrivate]", out bool negated, out ReadOnlyMemory typeName, out string? memberName)); + Assert.True(negated); + Assert.Equal("Microsoft.VisualStudio.Shell.Interop.IVsRunningDocumentTablePrivate", typeName.ToString()); + Assert.Null(memberName); + } + + [Fact] + public void EmptyString() + { + Assert.False(CommonInterestParsing.TryParseNegatableTypeOrMemberReference(string.Empty, out _, out _, out _)); + } + + [Fact] + public void NoBrackets() + { + Assert.False(CommonInterestParsing.TryParseNegatableTypeOrMemberReference("MyType", out _, out _, out _)); + } + + [Fact] + public void MissingOpeningBracket() + { + Assert.False(CommonInterestParsing.TryParseNegatableTypeOrMemberReference("MyType]", out _, out _, out _)); + } + + [Fact] + public void MissingClosingBracket() + { + Assert.False(CommonInterestParsing.TryParseNegatableTypeOrMemberReference("[MyType", out _, out _, out _)); + } + + [Fact] + public void EmptyTypeName() + { + Assert.False(CommonInterestParsing.TryParseNegatableTypeOrMemberReference("[]", out _, out _, out _)); + } + + [Fact] + public void SingleColonNotDouble() + { + Assert.False(CommonInterestParsing.TryParseNegatableTypeOrMemberReference("[MyType]:MyMethod", out _, out _, out _)); + } + + [Fact] + public void DoubleColonWithoutMemberName() + { + Assert.False(CommonInterestParsing.TryParseNegatableTypeOrMemberReference("[MyType]::", out _, out _, out _)); + } + + [Fact] + public void ColonInsideBrackets() + { + Assert.False(CommonInterestParsing.TryParseNegatableTypeOrMemberReference("[MyType::NotAllowed]", out _, out _, out _)); + } + + [Fact] + public void LeadingWhitespace() + { + Assert.False(CommonInterestParsing.TryParseNegatableTypeOrMemberReference(" [MyType]", out _, out _, out _)); + } + + [Fact] + public void SpaceInMiddleOfMemberName() + { + // The member name scanner stops at whitespace, so "extra" content after a space is not consumed + // and the trailing-only-whitespace check rejects the line. + Assert.False(CommonInterestParsing.TryParseNegatableTypeOrMemberReference("[MyType]::MyMethod extra", out _, out _, out _)); + } + + [Fact] + public void WildcardTypeName() + { + Assert.True(CommonInterestParsing.TryParseNegatableTypeOrMemberReference("[*]", out bool negated, out ReadOnlyMemory typeName, out string? memberName)); + Assert.False(negated); + Assert.Equal("*", typeName.ToString()); + Assert.Null(memberName); + } + } + + public class TryParseMemberReferenceTests + { + [Fact] + public void TypeAndMember() + { + Assert.True(CommonInterestParsing.TryParseMemberReference("[MyType]::MyMethod", out ReadOnlyMemory typeName, out string? memberName)); + Assert.Equal("MyType", typeName.ToString()); + Assert.Equal("MyMethod", memberName); + } + + [Fact] + public void TypeWithNamespaceAndMember() + { + Assert.True(CommonInterestParsing.TryParseMemberReference("[My.Namespace.MyType]::MyMethod", out ReadOnlyMemory typeName, out string? memberName)); + Assert.Equal("My.Namespace.MyType", typeName.ToString()); + Assert.Equal("MyMethod", memberName); + } + + [Fact] + public void TrailingWhitespace() + { + Assert.True(CommonInterestParsing.TryParseMemberReference("[MyType]::MyMethod ", out ReadOnlyMemory typeName, out string? memberName)); + Assert.Equal("MyType", typeName.ToString()); + Assert.Equal("MyMethod", memberName); + } + + [Fact] + public void EmptyString() + { + Assert.False(CommonInterestParsing.TryParseMemberReference(string.Empty, out _, out _)); + } + + [Fact] + public void TypeOnly_NoMember() + { + Assert.False(CommonInterestParsing.TryParseMemberReference("[MyType]", out _, out _)); + } + + [Fact] + public void NoBrackets() + { + Assert.False(CommonInterestParsing.TryParseMemberReference("MyType::MyMethod", out _, out _)); + } + + [Fact] + public void MissingClosingBracket() + { + Assert.False(CommonInterestParsing.TryParseMemberReference("[MyType::MyMethod", out _, out _)); + } + + [Fact] + public void EmptyTypeName() + { + Assert.False(CommonInterestParsing.TryParseMemberReference("[]::MyMethod", out _, out _)); + } + + [Fact] + public void SingleColonNotDouble() + { + Assert.False(CommonInterestParsing.TryParseMemberReference("[MyType]:MyMethod", out _, out _)); + } + + [Fact] + public void DoubleColonWithoutMemberName() + { + Assert.False(CommonInterestParsing.TryParseMemberReference("[MyType]::", out _, out _)); + } + + [Fact] + public void LeadingWhitespace() + { + Assert.False(CommonInterestParsing.TryParseMemberReference(" [MyType]::MyMethod", out _, out _)); + } + + [Fact] + public void Negation_NotSupported() + { + // TryParseMemberReference does not accept a leading '!'. + Assert.False(CommonInterestParsing.TryParseMemberReference("![MyType]::MyMethod", out _, out _)); + } + } +} diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Helpers/CSharpCodeFixVerifier`2+Test.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Helpers/CSharpCodeFixVerifier`2+Test.cs index 274b7b1de..b767fa30e 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Helpers/CSharpCodeFixVerifier`2+Test.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Helpers/CSharpCodeFixVerifier`2+Test.cs @@ -1,86 +1,86 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System; - using System.Collections.Immutable; - using System.IO; - using System.Linq; - using System.Reflection; - using System.Runtime.CompilerServices; - using System.Threading.Tasks; - using System.Windows.Threading; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CSharp; - using Microsoft.CodeAnalysis.CSharp.Testing; - using Microsoft.CodeAnalysis.Testing.Verifiers; - using Microsoft.CodeAnalysis.Text; - using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider; +using System; +using System.IO; +using System.Linq; +using System.Reflection; +#if WINDOWS +using System.Windows.Threading; +#endif +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Text; +using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider; + +namespace Microsoft.VisualStudio.Threading.Analyzers.Tests; - public static partial class CSharpCodeFixVerifier +public static partial class CSharpCodeFixVerifier +{ + public class Test : CSharpCodeFixTest { - public class Test : CSharpCodeFixTest + public Test() { - public Test() + this.ReferenceAssemblies = ReferencesHelper.DefaultReferences; + + this.SolutionTransforms.Add((solution, projectId) => { - this.ReferenceAssemblies = ReferencesHelper.DefaultReferences; + Project project = solution.GetProject(projectId)!; - this.SolutionTransforms.Add((solution, projectId) => + if (this.IncludeMicrosoftVisualStudioThreading) { - Project project = solution.GetProject(projectId)!; + project = project.AddMetadataReference(MetadataReference.CreateFromFile(typeof(JoinableTaskFactory).Assembly.Location)); + } - if (this.IncludeMicrosoftVisualStudioThreading) - { - project = project.AddMetadataReference(MetadataReference.CreateFromFile(typeof(JoinableTaskFactory).Assembly.Location)); - } + if (this.IncludeWindowsBase) + { +#if WINDOWS + project = project.AddMetadataReference(MetadataReference.CreateFromFile(typeof(Dispatcher).Assembly.Location)); +#else + Assert.SkipWhen(true, "Windows only"); +#endif + } - if (this.IncludeWindowsBase) - { - project = project.AddMetadataReference(MetadataReference.CreateFromFile(typeof(Dispatcher).Assembly.Location)); - } + if (this.IncludeVisualStudioSdk) + { + project = project.AddMetadataReference(MetadataReference.CreateFromFile(typeof(IOleServiceProvider).Assembly.Location)); - if (this.IncludeVisualStudioSdk) + var nugetPackagesFolder = Environment.CurrentDirectory; + foreach (var reference in ReferencesHelper.VSSDKPackageReferences) { - project = project.AddMetadataReference(MetadataReference.CreateFromFile(typeof(IOleServiceProvider).Assembly.Location)); - - var nugetPackagesFolder = Environment.CurrentDirectory; - foreach (var reference in ReferencesHelper.VSSDKPackageReferences) - { - project = project.AddMetadataReference(MetadataReference.CreateFromFile(Path.Combine(nugetPackagesFolder, reference))); - } + project = project.AddMetadataReference(MetadataReference.CreateFromFile(Path.Combine(nugetPackagesFolder, reference))); } + } - return project.Solution; - }); + return project.Solution; + }); - this.TestState.AdditionalFilesFactories.Add(() => - { - const string additionalFilePrefix = "AdditionalFiles."; - return from resourceName in Assembly.GetExecutingAssembly().GetManifestResourceNames() - where resourceName.StartsWith(additionalFilePrefix, StringComparison.Ordinal) - let content = ReadManifestResource(Assembly.GetExecutingAssembly(), resourceName) - select (filename: resourceName.Substring(additionalFilePrefix.Length), SourceText.From(content)); - }); - } + this.TestState.AdditionalFilesFactories.Add(() => + { + const string additionalFilePrefix = "AdditionalFiles."; + return from resourceName in Assembly.GetExecutingAssembly().GetManifestResourceNames() + where resourceName.StartsWith(additionalFilePrefix, StringComparison.Ordinal) + let content = ReadManifestResource(Assembly.GetExecutingAssembly(), resourceName) + select (filename: resourceName.Substring(additionalFilePrefix.Length), SourceText.From(content)); + }); + } - public bool IncludeMicrosoftVisualStudioThreading { get; set; } = true; + public bool IncludeMicrosoftVisualStudioThreading { get; set; } = true; - public bool IncludeWindowsBase { get; set; } = true; + public bool IncludeWindowsBase { get; set; } - public bool IncludeVisualStudioSdk { get; set; } = true; + public bool IncludeVisualStudioSdk { get; set; } = true; - protected override ParseOptions CreateParseOptions() - { - return ((CSharpParseOptions)base.CreateParseOptions()).WithLanguageVersion(LanguageVersion.CSharp8); - } + protected override ParseOptions CreateParseOptions() + { + return ((CSharpParseOptions)base.CreateParseOptions()).WithLanguageVersion(LanguageVersion.CSharp11); + } - private static string ReadManifestResource(Assembly assembly, string resourceName) + private static string ReadManifestResource(Assembly assembly, string resourceName) + { + using (var reader = new StreamReader(assembly.GetManifestResourceStream(resourceName) ?? throw Assumes.Fail("Resource not found."))) { - using (var reader = new StreamReader(assembly.GetManifestResourceStream(resourceName))) - { - return reader.ReadToEnd(); - } + return reader.ReadToEnd(); } } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Helpers/CSharpCodeFixVerifier`2.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Helpers/CSharpCodeFixVerifier`2.cs index 894ba2f83..57c5eb259 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Helpers/CSharpCodeFixVerifier`2.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Helpers/CSharpCodeFixVerifier`2.cs @@ -1,52 +1,47 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.Threading.Analyzers.Tests; + +public static partial class CSharpCodeFixVerifier + where TAnalyzer : DiagnosticAnalyzer, new() + where TCodeFix : CodeFixProvider, new() { - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CodeFixes; - using Microsoft.CodeAnalysis.CSharp.Testing; - using Microsoft.CodeAnalysis.Diagnostics; - using Microsoft.CodeAnalysis.Testing; - using Microsoft.CodeAnalysis.Testing.Verifiers; - - public static partial class CSharpCodeFixVerifier - where TAnalyzer : DiagnosticAnalyzer, new() - where TCodeFix : CodeFixProvider, new() - { - public static DiagnosticResult Diagnostic() - => CSharpCodeFixVerifier.Diagnostic(); + public static DiagnosticResult Diagnostic() + => CSharpCodeFixVerifier.Diagnostic(); - public static DiagnosticResult Diagnostic(string diagnosticId) - => CSharpCodeFixVerifier.Diagnostic(diagnosticId); + public static DiagnosticResult Diagnostic(string diagnosticId) + => CSharpCodeFixVerifier.Diagnostic(diagnosticId); - public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) - => new DiagnosticResult(descriptor); + public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) + => new DiagnosticResult(descriptor); - public static Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) - { - var test = new Test { TestCode = source }; - test.ExpectedDiagnostics.AddRange(expected); - return test.RunAsync(); - } + public static Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) + { + var test = new Test { TestCode = source }; + test.ExpectedDiagnostics.AddRange(expected); + return test.RunAsync(); + } - public static Task VerifyCodeFixAsync(string source, string fixedSource) - => VerifyCodeFixAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource); + public static Task VerifyCodeFixAsync(string source, string fixedSource) + => VerifyCodeFixAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource); - public static Task VerifyCodeFixAsync(string source, DiagnosticResult expected, string fixedSource) - => VerifyCodeFixAsync(source, new[] { expected }, fixedSource); + public static Task VerifyCodeFixAsync(string source, DiagnosticResult expected, string fixedSource) + => VerifyCodeFixAsync(source, new[] { expected }, fixedSource); - public static Task VerifyCodeFixAsync(string source, DiagnosticResult[] expected, string fixedSource) + public static Task VerifyCodeFixAsync(string source, DiagnosticResult[] expected, string fixedSource) + { + var test = new Test { - var test = new Test - { - TestCode = source, - FixedCode = fixedSource, - }; - - test.ExpectedDiagnostics.AddRange(expected); - return test.RunAsync(); - } + TestCode = source, + FixedCode = fixedSource, + }; + + test.ExpectedDiagnostics.AddRange(expected); + return test.RunAsync(); } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Helpers/ReferencesHelper.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Helpers/ReferencesHelper.cs index 747094d83..8c440cafb 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Helpers/ReferencesHelper.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Helpers/ReferencesHelper.cs @@ -1,31 +1,59 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests +#pragma warning disable SA1202 // Elements should be ordered by access - because field initializer depend on each other + +using System; +using System.Collections.Immutable; +using System.IO; +using System.Net; + +namespace Microsoft.VisualStudio.Threading.Analyzers.Tests; + +internal static class ReferencesHelper { - using System.Collections.Immutable; - using System.Net; - using Microsoft.CodeAnalysis.Testing; + private static readonly string NuGetConfigPath = FindNuGetConfigPath(); - internal static class ReferencesHelper - { - public static ReferenceAssemblies DefaultReferences = ReferenceAssemblies.Default - .WithPackages(ImmutableArray.Create( - new PackageIdentity("System.Collections.Immutable", "1.3.1"), - new PackageIdentity("System.Threading.Tasks.Extensions", "4.5.3"), - new PackageIdentity("Microsoft.Bcl.AsyncInterfaces", "1.1.0"))); +#if NETFRAMEWORK + public static ReferenceAssemblies DefaultReferences = ReferenceAssemblies.NetFramework.Net471.Default +#elif NET8_0 + public static ReferenceAssemblies DefaultReferences = ReferenceAssemblies.Net.Net80 +#else +#error Fix TFM conditions +#endif + .WithNuGetConfigFilePath(NuGetConfigPath) + .WithPackages(ImmutableArray.Create( + new PackageIdentity("System.Collections.Immutable", "6.0.0"), + new PackageIdentity("System.Threading.Tasks.Extensions", "4.6.3"), + new PackageIdentity("Microsoft.Bcl.AsyncInterfaces", "6.0.0"))); - internal static readonly ImmutableArray VSSDKPackageReferences = ImmutableArray.Create(new string[] - { - "Microsoft.VisualStudio.Shell.Framework.dll", - "Microsoft.VisualStudio.Shell.15.0.dll", - }); + internal static readonly ImmutableArray VSSDKPackageReferences = ImmutableArray.Create(new string[] + { + "Microsoft.VisualStudio.Shell.Framework.dll", + "Microsoft.VisualStudio.Shell.15.0.dll", + }); - static ReferencesHelper() - { + static ReferencesHelper() + { #pragma warning disable RS0030 // Do not used banned APIs - ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; #pragma warning restore RS0030 // Do not used banned APIs + } + + private static string FindNuGetConfigPath() + { + string? path = AppContext.BaseDirectory; + while (path is not null) + { + string candidate = Path.Combine(path, "nuget.config"); + if (File.Exists(candidate)) + { + return candidate; + } + + path = Path.GetDirectoryName(path); } + + throw new InvalidOperationException("Could not find NuGet.config by searching up from " + AppContext.BaseDirectory); } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Helpers/VisualBasicCodeFixVerifier`2+Test.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Helpers/VisualBasicCodeFixVerifier`2+Test.cs index ac51af335..c3a1ef718 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Helpers/VisualBasicCodeFixVerifier`2+Test.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Helpers/VisualBasicCodeFixVerifier`2+Test.cs @@ -1,84 +1,86 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System; - using System.Collections.Immutable; - using System.IO; - using System.Linq; - using System.Reflection; - using System.Windows.Threading; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Testing.Verifiers; - using Microsoft.CodeAnalysis.Text; - using Microsoft.CodeAnalysis.VisualBasic; - using Microsoft.CodeAnalysis.VisualBasic.Testing; - using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider; +using System; +using System.IO; +using System.Linq; +using System.Reflection; +#if WINDOWS +using System.Windows.Threading; +#endif +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.VisualBasic; +using Microsoft.CodeAnalysis.VisualBasic.Testing; +using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider; + +namespace Microsoft.VisualStudio.Threading.Analyzers.Tests; - public static partial class VisualBasicCodeFixVerifier +public static partial class VisualBasicCodeFixVerifier +{ + public class Test : VisualBasicCodeFixTest { - public class Test : VisualBasicCodeFixTest + public Test() { - public Test() + this.ReferenceAssemblies = ReferencesHelper.DefaultReferences; + + this.SolutionTransforms.Add((solution, projectId) => { - this.ReferenceAssemblies = ReferencesHelper.DefaultReferences; + Project? project = solution.GetProject(projectId) ?? throw new ArgumentException("Not found.", nameof(projectId)); - this.SolutionTransforms.Add((solution, projectId) => + if (this.IncludeMicrosoftVisualStudioThreading) { - Project? project = solution.GetProject(projectId) ?? throw new ArgumentException("Not found.", nameof(projectId)); + project = project.AddMetadataReference(MetadataReference.CreateFromFile(typeof(JoinableTaskFactory).Assembly.Location)); + } - if (this.IncludeMicrosoftVisualStudioThreading) - { - project = project.AddMetadataReference(MetadataReference.CreateFromFile(typeof(JoinableTaskFactory).Assembly.Location)); - } + if (this.IncludeWindowsBase) + { +#if WINDOWS + project = project.AddMetadataReference(MetadataReference.CreateFromFile(typeof(Dispatcher).Assembly.Location)); +#else + Assert.SkipWhen(true, "Windows only"); +#endif + } - if (this.IncludeWindowsBase) - { - project = project.AddMetadataReference(MetadataReference.CreateFromFile(typeof(Dispatcher).Assembly.Location)); - } + if (this.IncludeVisualStudioSdk) + { + project = project.AddMetadataReference(MetadataReference.CreateFromFile(typeof(IOleServiceProvider).Assembly.Location)); - if (this.IncludeVisualStudioSdk) + var nugetPackagesFolder = Environment.CurrentDirectory; + foreach (var reference in ReferencesHelper.VSSDKPackageReferences) { - project = project.AddMetadataReference(MetadataReference.CreateFromFile(typeof(IOleServiceProvider).Assembly.Location)); - - var nugetPackagesFolder = Environment.CurrentDirectory; - foreach (var reference in ReferencesHelper.VSSDKPackageReferences) - { - project = project.AddMetadataReference(MetadataReference.CreateFromFile(Path.Combine(nugetPackagesFolder, reference))); - } + project = project.AddMetadataReference(MetadataReference.CreateFromFile(Path.Combine(nugetPackagesFolder, reference))); } + } - return project.Solution; - }); + return project.Solution; + }); - this.TestState.AdditionalFilesFactories.Add(() => - { - const string additionalFilePrefix = "AdditionalFiles."; - return from resourceName in Assembly.GetExecutingAssembly().GetManifestResourceNames() - where resourceName.StartsWith(additionalFilePrefix, StringComparison.Ordinal) - let content = ReadManifestResource(Assembly.GetExecutingAssembly(), resourceName) - select (filename: resourceName.Substring(additionalFilePrefix.Length), SourceText.From(content)); - }); - } + this.TestState.AdditionalFilesFactories.Add(() => + { + const string additionalFilePrefix = "AdditionalFiles."; + return from resourceName in Assembly.GetExecutingAssembly().GetManifestResourceNames() + where resourceName.StartsWith(additionalFilePrefix, StringComparison.Ordinal) + let content = ReadManifestResource(Assembly.GetExecutingAssembly(), resourceName) + select (filename: resourceName.Substring(additionalFilePrefix.Length), SourceText.From(content)); + }); + } - public bool IncludeMicrosoftVisualStudioThreading { get; set; } = true; + public bool IncludeMicrosoftVisualStudioThreading { get; set; } = true; - public bool IncludeWindowsBase { get; set; } = true; + public bool IncludeWindowsBase { get; set; } - public bool IncludeVisualStudioSdk { get; set; } = true; + public bool IncludeVisualStudioSdk { get; set; } = true; - protected override ParseOptions CreateParseOptions() - { - return ((VisualBasicParseOptions)base.CreateParseOptions()).WithLanguageVersion(LanguageVersion.VisualBasic15_5); - } + protected override ParseOptions CreateParseOptions() + { + return ((VisualBasicParseOptions)base.CreateParseOptions()).WithLanguageVersion(LanguageVersion.VisualBasic15_5); + } - private static string ReadManifestResource(Assembly assembly, string resourceName) + private static string ReadManifestResource(Assembly assembly, string resourceName) + { + using (var reader = new StreamReader(assembly.GetManifestResourceStream(resourceName) ?? throw Assumes.Fail("Resource not found."))) { - using (var reader = new StreamReader(assembly.GetManifestResourceStream(resourceName))) - { - return reader.ReadToEnd(); - } + return reader.ReadToEnd(); } } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Helpers/VisualBasicCodeFixVerifier`2.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Helpers/VisualBasicCodeFixVerifier`2.cs index fcff374a9..7be9a7b25 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Helpers/VisualBasicCodeFixVerifier`2.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Helpers/VisualBasicCodeFixVerifier`2.cs @@ -1,52 +1,47 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.VisualBasic.Testing; + +namespace Microsoft.VisualStudio.Threading.Analyzers.Tests; + +public static partial class VisualBasicCodeFixVerifier + where TAnalyzer : DiagnosticAnalyzer, new() + where TCodeFix : CodeFixProvider, new() { - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CodeFixes; - using Microsoft.CodeAnalysis.Diagnostics; - using Microsoft.CodeAnalysis.Testing; - using Microsoft.CodeAnalysis.Testing.Verifiers; - using Microsoft.CodeAnalysis.VisualBasic.Testing; - - public static partial class VisualBasicCodeFixVerifier - where TAnalyzer : DiagnosticAnalyzer, new() - where TCodeFix : CodeFixProvider, new() - { - public static DiagnosticResult Diagnostic() - => VisualBasicCodeFixVerifier.Diagnostic(); + public static DiagnosticResult Diagnostic() + => VisualBasicCodeFixVerifier.Diagnostic(); - public static DiagnosticResult Diagnostic(string diagnosticId) - => VisualBasicCodeFixVerifier.Diagnostic(diagnosticId); + public static DiagnosticResult Diagnostic(string diagnosticId) + => VisualBasicCodeFixVerifier.Diagnostic(diagnosticId); - public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) - => new DiagnosticResult(descriptor); + public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) + => new DiagnosticResult(descriptor); - public static Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) - { - var test = new Test { TestCode = source }; - test.ExpectedDiagnostics.AddRange(expected); - return test.RunAsync(); - } + public static Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) + { + var test = new Test { TestCode = source }; + test.ExpectedDiagnostics.AddRange(expected); + return test.RunAsync(); + } - public static Task VerifyCodeFixAsync(string source, string fixedSource) - => VerifyCodeFixAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource); + public static Task VerifyCodeFixAsync(string source, string fixedSource) + => VerifyCodeFixAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource); - public static Task VerifyCodeFixAsync(string source, DiagnosticResult expected, string fixedSource) - => VerifyCodeFixAsync(source, new[] { expected }, fixedSource); + public static Task VerifyCodeFixAsync(string source, DiagnosticResult expected, string fixedSource) + => VerifyCodeFixAsync(source, new[] { expected }, fixedSource); - public static Task VerifyCodeFixAsync(string source, DiagnosticResult[] expected, string fixedSource) + public static Task VerifyCodeFixAsync(string source, DiagnosticResult[] expected, string fixedSource) + { + var test = new Test { - var test = new Test - { - TestCode = source, - FixedCode = fixedSource, - }; - - test.ExpectedDiagnostics.AddRange(expected); - return test.RunAsync(); - } + TestCode = source, + FixedCode = fixedSource, + }; + + test.ExpectedDiagnostics.AddRange(expected); + return test.RunAsync(); } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Microsoft.VisualStudio.Threading.Analyzers.Tests.csproj b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Microsoft.VisualStudio.Threading.Analyzers.Tests.csproj index 223a8c2db..a0a354341 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Microsoft.VisualStudio.Threading.Analyzers.Tests.csproj +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Microsoft.VisualStudio.Threading.Analyzers.Tests.csproj @@ -1,13 +1,20 @@  - net472 - true + net8.0 + Exe true true + $(NoWarn);NU1701 + $(DefineConstants);WINDOWS + App.net472.config + true - + + + $(TargetFrameworks);net8.0-windows;net472 + @@ -16,16 +23,17 @@ - - - - - - - + + + + + + + - + AdditionalFiles.%(FileName)%(Extension) @@ -34,6 +42,12 @@ + + + + + + @@ -43,4 +57,13 @@ AdditionalFiles + + + + + + + diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/MultiAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/MultiAnalyzerTests.cs index b9a77dc4c..9d4a61437 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/MultiAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/MultiAnalyzerTests.cs @@ -1,25 +1,20 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.VisualStudio.Threading.Analyzers.Tests; +using CSVerify = MultiAnalyzerTests.Verifier; + +public class MultiAnalyzerTests { - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Linq; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Diagnostics; - using Microsoft.CodeAnalysis.Testing; - using Xunit; - using Verify = MultiAnalyzerTests.Verifier; - - public class MultiAnalyzerTests + [Fact] + public async Task JustOneDiagnosticPerLine() { - [Fact] - public async Task JustOneDiagnosticPerLine() - { - var test = @" + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -42,39 +37,39 @@ static void SetTaskSourceIfCompleted(Task task, TaskCompletionSource tc } }"; - DiagnosticResult[] expected = - { - Verify.Diagnostic(VSTHRD103UseAsyncOptionAnalyzer.DescriptorNoAlternativeMethod).WithSpan(10, 24, 10, 33).WithArguments("GetResult"), - Verify.Diagnostic(VSTHRD103UseAsyncOptionAnalyzer.Descriptor).WithSpan(11, 13, 11, 16).WithArguments("Run", "RunAsync"), - Verify.Diagnostic(VSTHRD002UseJtfRunAnalyzer.Descriptor).WithSpan(19, 32, 19, 38), - }; + DiagnosticResult[] expected = + { + CSVerify.Diagnostic(VSTHRD103UseAsyncOptionAnalyzer.DescriptorNoAlternativeMethod).WithSpan(10, 24, 10, 33).WithArguments("GetResult"), + CSVerify.Diagnostic(VSTHRD103UseAsyncOptionAnalyzer.Descriptor).WithSpan(11, 13, 11, 16).WithArguments("Run", "RunAsync"), + CSVerify.Diagnostic(VSTHRD002UseJtfRunAnalyzer.Descriptor).WithSpan(19, 32, 19, 38), + }; - // All expected diagnostics should include a location - Assert.All(expected, item => Assert.True(item.HasLocation)); + // All expected diagnostics should include a location + Assert.All(expected, item => Assert.True(item.HasLocation)); - // All diagnostics should fit on one line - Assert.All(expected, item => Assert.Equal(item.Spans[0].Span.EndLinePosition.Line, item.Spans[0].Span.StartLinePosition.Line)); + // All diagnostics should fit on one line + Assert.All(expected, item => Assert.Equal(item.Spans[0].Span.EndLinePosition.Line, item.Spans[0].Span.StartLinePosition.Line)); - // At most one diagnostic appears on any given line - Assert.Equal(expected.Length, expected.Select(d => d.Spans[0].Span.StartLinePosition.Line).Distinct().Count()); + // At most one diagnostic appears on any given line + Assert.Equal(expected.Length, expected.Select(d => d.Spans[0].Span.StartLinePosition.Line).Distinct().Count()); - var verifyTest = new Verify.Test - { - TestCode = test, - TestState = { MarkupHandling = MarkupMode.None }, - }; + var verifyTest = new CSVerify.Test + { + TestCode = test, + TestState = { MarkupHandling = MarkupMode.None }, + }; - verifyTest.ExpectedDiagnostics.AddRange(expected); - await verifyTest.RunAsync(); - } + verifyTest.ExpectedDiagnostics.AddRange(expected); + await verifyTest.RunAsync(); + } - /// - /// Verifies that no analyzer throws due to a missing interface member. - /// - [Fact] - public async Task MissingInterfaceImplementationMember() - { - var test = @" + /// + /// Verifies that no analyzer throws due to a missing interface member. + /// + [Fact] + public async Task MissingInterfaceImplementationMember() + { + var test = @" public interface A { void Foo(); } @@ -88,14 +83,14 @@ public Child() { } } "; - DiagnosticResult expected = Verify.CompilerError("CS0535").WithLocation(6, 23).WithArguments("Parent", "A.Foo()"); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.CompilerError("CS0535").WithLocation(6, 23).WithArguments("Parent", "A.Foo()"); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task AnonymousTypeObjectCreationSyntax() - { - var test = @" + [Fact] + public async Task AnonymousTypeObjectCreationSyntax() + { + var test = @" using System; public class A { @@ -109,13 +104,13 @@ internal void C() { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task MissingTypeObjectCreationSyntax() - { - var test = @" + [Fact] + public async Task MissingTypeObjectCreationSyntax() + { + var test = @" using System; public class A { @@ -129,18 +124,18 @@ internal void C() { } "; - DiagnosticResult[] expected = - { - Verify.CompilerError("CS0246").WithLocation(6, 21).WithArguments("C"), - Verify.CompilerError("CS0246").WithLocation(10, 21).WithArguments("C"), - }; - await Verify.VerifyAnalyzerAsync(test, expected); - } - - [Fact] - public async Task ManyMethodInvocationStyles() + DiagnosticResult[] expected = { - var test = @" + CSVerify.CompilerError("CS0246").WithLocation(6, 21).WithArguments("C"), + CSVerify.CompilerError("CS0246").WithLocation(10, 21).WithArguments("C"), + }; + await CSVerify.VerifyAnalyzerAsync(test, expected); + } + + [Fact] + public async Task ManyMethodInvocationStyles() + { + var test = @" using System; using System.Threading.Tasks; @@ -191,13 +186,13 @@ private void D() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task UseOf_XmlDocRefs_DoesNotProduceWarnings() - { - var test = @" + [Fact] + public async Task UseOf_XmlDocRefs_DoesNotProduceWarnings() + { + var test = @" using System; using System.Threading.Tasks; @@ -213,13 +208,13 @@ public void PublicFoo() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task UseOf_nameof_DoesNotProduceWarnings() - { - var test = @" + [Fact] + public async Task UseOf_nameof_DoesNotProduceWarnings() + { + var test = @" using System; using System.Threading.Tasks; @@ -241,13 +236,13 @@ public void PublicFoo() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task UseOf_Delegate_DoesNotProduceWarnings() - { - var test = @" + [Fact] + public async Task UseOf_Delegate_DoesNotProduceWarnings() + { + var test = @" using System; using System.Threading.Tasks; @@ -263,82 +258,81 @@ public void PublicFoo() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - /// - /// Verifies that no reference to System.ValueTuple exists, - /// so we know the analyzers will work on VS2015. - /// - /// - /// We have to reference the assembly during compilation due to - /// https://github.com/dotnet/roslyn/issues/18629 - /// So this unit test guards that we don't accidentally require the assembly - /// at runtime. - /// - [Fact] - public void NoValueTupleReference() - { - System.Reflection.AssemblyName[]? refAssemblies = typeof(CSharpVSTHRD001UseSwitchToMainThreadAsyncAnalyzer) - .Assembly.GetReferencedAssemblies(); - Assert.DoesNotContain(refAssemblies, a => a.Name.Equals("System.ValueTuple", StringComparison.OrdinalIgnoreCase)); - } + /// + /// Verifies that no reference to System.ValueTuple exists, + /// so we know the analyzers will work on VS2015. + /// + /// + /// We have to reference the assembly during compilation due to + /// https://github.com/dotnet/roslyn/issues/18629 + /// So this unit test guards that we don't accidentally require the assembly + /// at runtime. + /// + [Fact] + public void NoValueTupleReference() + { + System.Reflection.AssemblyName[]? refAssemblies = typeof(CSharpVSTHRD001UseSwitchToMainThreadAsyncAnalyzer) + .Assembly.GetReferencedAssemblies(); + Assert.DoesNotContain(refAssemblies, a => a.Name!.Equals("System.ValueTuple", StringComparison.OrdinalIgnoreCase)); + } - /// - /// Verifies that no reference to exists, - /// so we know the analyzers will work on .NET Framework versions that did not include it. - /// - /// - /// We reference the assembly during compilation for convenient use of nameof. - /// This unit test guards that we don't accidentally require the assembly - /// at runtime. - /// - [Fact] - public void NoValueTaskReference() - { - System.Reflection.AssemblyName[]? refAssemblies = typeof(CSharpVSTHRD001UseSwitchToMainThreadAsyncAnalyzer) - .Assembly.GetReferencedAssemblies(); - Assert.DoesNotContain(refAssemblies, a => a.Name.Equals("System.Threading.Tasks.Extensions", StringComparison.OrdinalIgnoreCase)); - } + /// + /// Verifies that no reference to exists, + /// so we know the analyzers will work on .NET Framework versions that did not include it. + /// + /// + /// We reference the assembly during compilation for convenient use of nameof. + /// This unit test guards that we don't accidentally require the assembly + /// at runtime. + /// + [Fact] + public void NoValueTaskReference() + { + System.Reflection.AssemblyName[]? refAssemblies = typeof(CSharpVSTHRD001UseSwitchToMainThreadAsyncAnalyzer) + .Assembly.GetReferencedAssemblies(); + Assert.DoesNotContain(refAssemblies, a => a.Name!.Equals("System.Threading.Tasks.Extensions", StringComparison.OrdinalIgnoreCase)); + } - [Fact] - public async Task NameOfUsedInAttributeArgument() - { - var test = @" + [Fact] + public async Task NameOfUsedInAttributeArgument() + { + var test = @" [System.Diagnostics.DebuggerDisplay(""hi"", Name = nameof(System.Console))] class Foo { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - public static class Verifier - { - public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) - => new DiagnosticResult(descriptor); + public static class Verifier + { + public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) + => new DiagnosticResult(descriptor); - public static DiagnosticResult CompilerError(string errorIdentifier) - => new DiagnosticResult(errorIdentifier, DiagnosticSeverity.Error); + public static DiagnosticResult CompilerError(string errorIdentifier) + => new DiagnosticResult(errorIdentifier, DiagnosticSeverity.Error); - public static Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) + public static Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) + { + var test = new Test { - var test = new Test - { - TestCode = source, - }; + TestCode = source, + }; - test.ExpectedDiagnostics.AddRange(expected); - return test.RunAsync(); - } + test.ExpectedDiagnostics.AddRange(expected); + return test.RunAsync(); + } - public class Test : CSharpCodeFixVerifier.Test + public class Test : CSharpCodeFixVerifier.Test + { + protected override IEnumerable GetDiagnosticAnalyzers() { - protected override IEnumerable GetDiagnosticAnalyzers() - { - IEnumerable? analyzers = from type in typeof(VSTHRD002UseJtfRunAnalyzer).Assembly.GetTypes() - where type.GetCustomAttributes(typeof(DiagnosticAnalyzerAttribute), true).Any() - select (DiagnosticAnalyzer)Activator.CreateInstance(type); - return analyzers.ToImmutableArray(); - } + IEnumerable? analyzers = from type in typeof(VSTHRD002UseJtfRunAnalyzer).Assembly.GetTypes() + where type.GetCustomAttributes(typeof(DiagnosticAnalyzerAttribute), true).Any() + select (DiagnosticAnalyzer?)Activator.CreateInstance(type) ?? throw Assumes.Fail("Unable to instantiate the analyzer"); + return analyzers.ToImmutableArray(); } } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Properties/AssemblyInfo.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Properties/AssemblyInfo.cs index 5a990af98..f19ad19bb 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Properties/AssemblyInfo.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Runtime.InteropServices; diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Usings.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Usings.cs new file mode 100644 index 000000000..4fb29506c --- /dev/null +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/Usings.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +global using System; +global using System.Threading.Tasks; +global using Microsoft; +global using Microsoft.CodeAnalysis; +global using Microsoft.CodeAnalysis.Testing; +global using Microsoft.VisualStudio.Threading.Analyzers; +global using Xunit; diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD001UseSwitchToMainThreadAsyncAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD001UseSwitchToMainThreadAsyncAnalyzerTests.cs index 2aefb06cd..0db4b9e29 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD001UseSwitchToMainThreadAsyncAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD001UseSwitchToMainThreadAsyncAnalyzerTests.cs @@ -1,18 +1,14 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System.Threading.Tasks; - using Xunit; - using Verify = CSharpCodeFixVerifier; +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; - public class VSTHRD001UseSwitchToMainThreadAsyncAnalyzerTests +public class VSTHRD001UseSwitchToMainThreadAsyncAnalyzerTests +{ + [Fact] + public async Task ThreadHelperInvoke_ProducesDiagnostic() { - [Fact] - public async Task ThreadHelperInvoke_ProducesDiagnostic() - { - var test = @" + var test = @" using Microsoft.VisualStudio.Shell; class Test { @@ -22,13 +18,13 @@ void Foo() { } "; - await Verify.VerifyAnalyzerAsync(test, Verify.Diagnostic().WithLocation(0)); - } + await CSVerify.VerifyAnalyzerAsync(test, CSVerify.Diagnostic().WithLocation(0)); + } - [Fact] - public async Task ThreadHelperBeginInvoke_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task ThreadHelperBeginInvoke_ProducesDiagnostic() + { + var test = @" using Microsoft.VisualStudio.Shell; class Test { @@ -38,13 +34,13 @@ void Foo() { } "; - await Verify.VerifyAnalyzerAsync(test, Verify.Diagnostic().WithLocation(0)); - } + await CSVerify.VerifyAnalyzerAsync(test, CSVerify.Diagnostic().WithLocation(0)); + } - [Fact] - public async Task ThreadHelperInvokeAsync_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task ThreadHelperInvokeAsync_ProducesDiagnostic() + { + var test = @" using Microsoft.VisualStudio.Shell; class Test { @@ -54,13 +50,13 @@ void Foo() { } "; - await Verify.VerifyAnalyzerAsync(test, Verify.Diagnostic().WithLocation(0)); - } + await CSVerify.VerifyAnalyzerAsync(test, CSVerify.Diagnostic().WithLocation(0)); + } - [Fact] - public async Task DispatcherInvoke_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task DispatcherInvoke_ProducesDiagnostic() + { + var test = @" using System.Windows.Threading; class Test { @@ -70,13 +66,15 @@ void Foo() { } "; - await Verify.VerifyAnalyzerAsync(test, Verify.Diagnostic().WithLocation(0)); - } + var t = new CSVerify.Test { TestCode = test, IncludeWindowsBase = true }; + t.ExpectedDiagnostics.Add(CSVerify.Diagnostic().WithLocation(0)); + await t.RunAsync(); + } - [Fact] - public async Task DispatcherBeginInvoke_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task DispatcherBeginInvoke_ProducesDiagnostic() + { + var test = @" using System; using System.Windows.Threading; @@ -87,13 +85,15 @@ void Foo() { } "; - await Verify.VerifyAnalyzerAsync(test, Verify.Diagnostic().WithLocation(0)); - } + var t = new CSVerify.Test { TestCode = test, IncludeWindowsBase = true }; + t.ExpectedDiagnostics.Add(CSVerify.Diagnostic().WithLocation(0)); + await t.RunAsync(); + } - [Fact] - public async Task DispatcherInvokeAsync_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task DispatcherInvokeAsync_ProducesDiagnostic() + { + var test = @" using System.Windows.Threading; class Test { @@ -103,13 +103,15 @@ void Foo() { } "; - await Verify.VerifyAnalyzerAsync(test, Verify.Diagnostic().WithLocation(0)); - } + var t = new CSVerify.Test { TestCode = test, IncludeWindowsBase = true }; + t.ExpectedDiagnostics.Add(CSVerify.Diagnostic().WithLocation(0)); + await t.RunAsync(); + } - [Fact] - public async Task SynchronizationContextSend_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task SynchronizationContextSend_ProducesDiagnostic() + { + var test = @" using System.Threading; class Test { @@ -119,13 +121,13 @@ void Foo() { } "; - await Verify.VerifyAnalyzerAsync(test, Verify.Diagnostic().WithLocation(0)); - } + await CSVerify.VerifyAnalyzerAsync(test, CSVerify.Diagnostic().WithLocation(0)); + } - [Fact] - public async Task SynchronizationContextPost_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task SynchronizationContextPost_ProducesDiagnostic() + { + var test = @" using System.Threading; class Test { @@ -135,7 +137,6 @@ void Foo() { } "; - await Verify.VerifyAnalyzerAsync(test, Verify.Diagnostic().WithLocation(0)); - } + await CSVerify.VerifyAnalyzerAsync(test, CSVerify.Diagnostic().WithLocation(0)); } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD002UseJtfRunAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD002UseJtfRunAnalyzerTests.cs index 5f47ade45..eaaafbdf3 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD002UseJtfRunAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD002UseJtfRunAnalyzerTests.cs @@ -1,26 +1,20 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; + +public class VSTHRD002UseJtfRunAnalyzerTests { - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Testing; - using Xunit; - using Verify = CSharpCodeFixVerifier; - - public class VSTHRD002UseJtfRunAnalyzerTests - { - /// - /// We set TestCategory=AnyCategory here so that *some* test in our assembly uses - /// "TestCategory" as the name of a trait. This prevents VSTest.Console from failing - /// when invoked with /TestCaseFilter:"TestCategory!=FailsInCloudTest" for assemblies - /// such as this one that don't define any TestCategory tests. - /// - [Fact, Trait("TestCategory", "AnyCategory-SeeComment")] - public async Task TaskWaitShouldReportWarning() - { - var test = @" + /// + /// We set TestCategory=AnyCategory here so that *some* test in our assembly uses + /// "TestCategory" as the name of a trait. This prevents VSTest.Console from failing + /// when invoked with /TestCaseFilter:"TestCategory!=FailsInCloudTest" for assemblies + /// such as this one that don't define any TestCategory tests. + /// + [Fact, Trait("TestCategory", "AnyCategory-SeeComment")] + public async Task TaskWaitShouldReportWarning() + { + var test = @" using System; using System.Threading.Tasks; @@ -31,7 +25,7 @@ void F() { } } "; - var withFix = @" + var withFix = @" using System; using System.Threading.Tasks; @@ -42,14 +36,14 @@ async Task FAsync() { } } "; - DiagnosticResult expected = Verify.Diagnostic().WithSpan(8, 14, 8, 18); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithSpan(8, 14, 8, 18); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task TaskWaitAnyShouldReportWarning() - { - var test = @" + [Fact] + public async Task TaskWaitAnyShouldReportWarning() + { + var test = @" using System; using System.Threading.Tasks; @@ -61,7 +55,7 @@ void F() { } } "; - var withFix = @" + var withFix = @" using System; using System.Threading.Tasks; @@ -73,14 +67,14 @@ async Task FAsync() { } } "; - DiagnosticResult expected = Verify.Diagnostic().WithSpan(9, 14, 9, 21); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithSpan(9, 14, 9, 21); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task TaskWhenAll_CompareWithAndWithout() - { - var test = @" + [Fact] + public async Task TaskWhenAll_CompareWithAndWithout() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -112,20 +106,20 @@ void WhenAll_NoWarnings() { } } "; - DiagnosticResult[] expected = - { - Verify.Diagnostic().WithSpan(14, 19, 14, 23), - Verify.Diagnostic().WithSpan(15, 19, 15, 23), - Verify.Diagnostic().WithSpan(16, 26, 16, 32), - Verify.Diagnostic().WithSpan(16, 54, 16, 63), - }; - await Verify.VerifyAnalyzerAsync(test, expected); - } - - [Fact] - public async Task TaskWhenAll_Multiple_NoWarning() + DiagnosticResult[] expected = { - var test = @" + CSVerify.Diagnostic().WithSpan(14, 19, 14, 23), + CSVerify.Diagnostic().WithSpan(15, 19, 15, 23), + CSVerify.Diagnostic().WithSpan(16, 26, 16, 32), + CSVerify.Diagnostic().WithSpan(16, 54, 16, 63), + }; + await CSVerify.VerifyAnalyzerAsync(test, expected); + } + + [Fact] + public async Task TaskWhenAll_Multiple_NoWarning() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -151,13 +145,13 @@ void Foo() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task TaskWhenAll_AfterResult_GeneratesWarning() - { - var test = @" + [Fact] + public async Task TaskWhenAll_AfterResult_GeneratesWarning() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -176,14 +170,14 @@ void Foo() { } } "; - DiagnosticResult expected = Verify.Diagnostic().WithSpan(13, 29, 13, 35); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithSpan(13, 29, 13, 35); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task TaskWhenAll_DifferentResult_GeneratesWarning() - { - var test = @" + [Fact] + public async Task TaskWhenAll_DifferentResult_GeneratesWarning() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -206,14 +200,14 @@ void Foo() { } } "; - DiagnosticResult expected = Verify.Diagnostic().WithSpan(18, 26, 18, 32); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithSpan(18, 26, 18, 32); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task TaskWhenAll_TaskPassedByValue_NoWarning() - { - var test = @" + [Fact] + public async Task TaskWhenAll_TaskPassedByValue_NoWarning() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -235,13 +229,13 @@ void Foo() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task TaskWhenAll_TaskPassedByRef_GeneratesWarning() - { - var test = @" + [Fact] + public async Task TaskWhenAll_TaskPassedByRef_GeneratesWarning() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -263,14 +257,14 @@ void Foo() { } } "; - DiagnosticResult expected = Verify.Diagnostic().WithSpan(18, 26, 18, 32); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithSpan(18, 26, 18, 32); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task TaskWhenAll_TaskPassedWithOut_GeneratesWarning() - { - var test = @" + [Fact] + public async Task TaskWhenAll_TaskPassedWithOut_GeneratesWarning() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -292,14 +286,14 @@ void Foo() { } } "; - DiagnosticResult expected = Verify.Diagnostic().WithSpan(18, 26, 18, 32); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithSpan(18, 26, 18, 32); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task TaskWhenAll_TaskVariableReused_GeneratesWarning() - { - var test = @" + [Fact] + public async Task TaskWhenAll_TaskVariableReused_GeneratesWarning() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -320,14 +314,14 @@ void Foo() { } } "; - DiagnosticResult expected = Verify.Diagnostic().WithSpan(17, 32, 17, 38); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithSpan(17, 32, 17, 38); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task TaskWhenAll_MultipleWhenAll_TaskVariableReused_GeneratesWarning() - { - var test = @" + [Fact] + public async Task TaskWhenAll_MultipleWhenAll_TaskVariableReused_GeneratesWarning() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -352,14 +346,14 @@ void Foo() { } } "; - DiagnosticResult expected = Verify.Diagnostic().WithSpan(21, 32, 21, 38); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithSpan(21, 32, 21, 38); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task TaskWaitAllShouldReportWarning() - { - var test = @" + [Fact] + public async Task TaskWaitAllShouldReportWarning() + { + var test = @" using System; using System.Threading.Tasks; @@ -371,7 +365,7 @@ void F() { } } "; - var withFix = @" + var withFix = @" using System; using System.Threading.Tasks; @@ -383,14 +377,14 @@ async Task FAsync() { } } "; - DiagnosticResult expected = Verify.Diagnostic().WithSpan(9, 14, 9, 21); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithSpan(9, 14, 9, 21); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task TaskWaitShouldReportWarning_WithinAnonymousDelegate() - { - var test = @" + [Fact] + public async Task TaskWaitShouldReportWarning_WithinAnonymousDelegate() + { + var test = @" using System; using System.Threading.Tasks; @@ -401,14 +395,14 @@ void F() { } } "; - DiagnosticResult expected = Verify.Diagnostic().WithSpan(8, 31, 8, 35); - await Verify.VerifyCodeFixAsync(test, expected, test); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithSpan(8, 31, 8, 35); + await CSVerify.VerifyCodeFixAsync(test, expected, test); + } - [Fact] - public async Task Task_Result_ShouldReportWarning() - { - var test = @" + [Fact] + public async Task Task_Result_ShouldReportWarning() + { + var test = @" using System; using System.Threading.Tasks; @@ -419,7 +413,7 @@ void F() { } } "; - var withFix = @" + var withFix = @" using System; using System.Threading.Tasks; @@ -430,14 +424,14 @@ async Task FAsync() { } } "; - DiagnosticResult expected = Verify.Diagnostic().WithSpan(8, 27, 8, 33); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithSpan(8, 27, 8, 33); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task ValueTask_Result_ShouldReportWarning() - { - var test = @" + [Fact] + public async Task ValueTask_Result_ShouldReportWarning() + { + var test = @" using System; using System.Threading.Tasks; @@ -448,7 +442,7 @@ void F() { } } "; - var withFix = @" + var withFix = @" using System; using System.Threading.Tasks; @@ -459,14 +453,14 @@ async Task FAsync() { } } "; - DiagnosticResult expected = Verify.Diagnostic().WithSpan(8, 27, 8, 33); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithSpan(8, 27, 8, 33); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task TaskResultShouldReportWarning_WithinAnonymousDelegate() - { - var test = @" + [Fact] + public async Task TaskResultShouldReportWarning_WithinAnonymousDelegate() + { + var test = @" using System; using System.Threading.Tasks; @@ -477,14 +471,14 @@ void F() { } } "; - DiagnosticResult expected = Verify.Diagnostic().WithSpan(8, 34, 8, 40); - await Verify.VerifyCodeFixAsync(test, expected, test); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithSpan(8, 34, 8, 40); + await CSVerify.VerifyCodeFixAsync(test, expected, test); + } - [Fact] - public async Task TaskResultShouldNotReportWarning_WithinItsOwnContinuationDelegate() - { - var test = @" + [Fact] + public async Task TaskResultShouldNotReportWarning_WithinItsOwnContinuationDelegate() + { + var test = @" using System; using System.Threading.Tasks; @@ -506,19 +500,19 @@ void ContinueWith(Func, int> del) { } } "; - DiagnosticResult[] expected = - { - Verify.Diagnostic().WithSpan(9, 47, 9, 53), - Verify.Diagnostic().WithSpan(10, 29, 10, 35), - }; + DiagnosticResult[] expected = + { + CSVerify.Diagnostic().WithSpan(9, 47, 9, 53), + CSVerify.Diagnostic().WithSpan(10, 29, 10, 35), + }; - await Verify.VerifyCodeFixAsync(test, expected, test); - } + await CSVerify.VerifyCodeFixAsync(test, expected, test); + } - [Fact] - public async Task Task_GetAwaiter_GetResult_ShouldReportWarning() - { - var test = @" + [Fact] + public async Task Task_GetAwaiter_GetResult_ShouldReportWarning() + { + var test = @" using System; using System.Threading.Tasks; @@ -529,7 +523,7 @@ void F() { } } "; - var withFix = @" + var withFix = @" using System; using System.Threading.Tasks; @@ -540,14 +534,42 @@ async Task FAsync() { } } "; - DiagnosticResult expected = Verify.Diagnostic().WithSpan(8, 27, 8, 36); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithSpan(8, 27, 8, 36); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task ValueTask_GetAwaiter_GetResult_ShouldReportWarning() - { - var test = @" + [Fact] + public async Task ConfiguredTask_GetAwaiter_GetResult_ShouldReportWarning() + { + var test = @" +using System; +using System.Threading.Tasks; + +class Test { + void F() { + var task = Task.Run(() => 1); + task.ConfigureAwait(false).GetAwaiter().[|GetResult|](); + } +} +"; + var withFix = @" +using System; +using System.Threading.Tasks; + +class Test { + async Task FAsync() { + var task = Task.Run(() => 1); + await task.ConfigureAwait(false); + } +} +"; + await CSVerify.VerifyCodeFixAsync(test, withFix); + } + + [Fact] + public async Task ValueTask_GetAwaiter_GetResult_ShouldReportWarning() + { + var test = @" using System; using System.Threading.Tasks; @@ -558,7 +580,7 @@ void F() { } } "; - var withFix = @" + var withFix = @" using System; using System.Threading.Tasks; @@ -569,16 +591,44 @@ async Task FAsync() { } } "; - DiagnosticResult expected = Verify.Diagnostic().WithSpan(8, 27, 8, 36); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithSpan(8, 27, 8, 36); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } + + [Fact] + public async Task ConfiguredValueTask_GetAwaiter_GetResult_ShouldReportWarning() + { + var test = @" +using System; +using System.Threading.Tasks; + +class Test { + void F() { + ValueTask task = default; + task.ConfigureAwait(false).GetAwaiter().[|GetResult|](); + } +} +"; + var withFix = @" +using System; +using System.Threading.Tasks; + +class Test { + async Task FAsync() { + ValueTask task = default; + await task.ConfigureAwait(false); + } +} +"; + await CSVerify.VerifyCodeFixAsync(test, withFix); + } - [Fact] - public async Task TaskResult_FixUpdatesCallers() + [Fact] + public async Task TaskResult_FixUpdatesCallers() + { + var test = new SourceFileList("Test", "cs") { - var test = new SourceFileList("Test", "cs") - { - @" + @" using System; using System.Threading.Tasks; @@ -602,17 +652,17 @@ static int Main(string[] args) } } ", - @" + @" class TestClient { int Multiply(int a, int b) { return Test.GetNumber(a) * b; } } ", - }; - var withFix = new SourceFileList("Test", "cs") - { - @" + }; + var withFix = new SourceFileList("Test", "cs") + { + @" using System; using System.Threading.Tasks; @@ -636,36 +686,36 @@ static async Task Main(string[] args) } } ", - @" + @" class TestClient { async System.Threading.Tasks.Task MultiplyAsync(int a, int b) { return await Test.GetNumberAsync(a) * b; } } ", - }; + }; - var verifyTest = new Verify.Test + var verifyTest = new CSVerify.Test + { + TestState = { - TestState = - { - OutputKind = OutputKind.ConsoleApplication, - }, - ExpectedDiagnostics = - { - Verify.Diagnostic().WithSpan("Test0.cs", 8, 21, 8, 27), - }, - }; - - verifyTest.TestState.Sources.AddRange(test); - verifyTest.FixedState.Sources.AddRange(withFix); - await verifyTest.RunAsync(); - } + OutputKind = OutputKind.ConsoleApplication, + }, + ExpectedDiagnostics = + { + CSVerify.Diagnostic().WithSpan("Test0.cs", 8, 21, 8, 27), + }, + }; - [Fact] - public async Task DoNotReportWarningInTaskReturningMethods() - { - var test = @" + verifyTest.TestState.Sources.AddRange(test); + verifyTest.FixedState.Sources.AddRange(withFix); + await verifyTest.RunAsync(); + } + + [Fact] + public async Task DoNotReportWarningInTaskReturningMethods() + { + var test = @" using System.Threading.Tasks; class Test { @@ -676,13 +726,13 @@ Task F() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task DoNotReportWarningOnCodeGeneratedByXaml2CS() - { - var test = @" + [Fact] + public async Task DoNotReportWarningOnCodeGeneratedByXaml2CS() + { + var test = @" //------------------------------------------------------------------------------ // // This code was generated by a tool. @@ -706,13 +756,13 @@ void F() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task DoNotReportWarningOnJTFRun() - { - var test = @" + [Fact] + public async Task DoNotReportWarningOnJTFRun() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -727,13 +777,13 @@ void F() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task DoNotReportWarningOnJoinableTaskJoin() - { - var test = @" + [Fact] + public async Task DoNotReportWarningOnJoinableTaskJoin() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -749,13 +799,13 @@ void F() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task MethodsWithoutLeadingMember() - { - var test = @" + [Fact] + public async Task MethodsWithoutLeadingMember() + { + var test = @" using System; using System.Threading; using System.Threading.Tasks; @@ -772,13 +822,13 @@ public void Start(Task action, Action exceptionHandler = null) } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task AnonymousDelegateWithExplicitCast() - { - var test = @" + [Fact] + public async Task AnonymousDelegateWithExplicitCast() + { + var test = @" using System; using System.Threading; using System.Threading.Tasks; @@ -796,7 +846,6 @@ public void Start(JoinableTask joinableTask, object registration) } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD003UseJtfRunAsyncAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD003UseJtfRunAsyncAnalyzerTests.cs index 8fd19cc15..ce64483d5 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD003UseJtfRunAsyncAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD003UseJtfRunAsyncAnalyzerTests.cs @@ -1,23 +1,14 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; + +public class VSTHRD003UseJtfRunAsyncAnalyzerTests { - using System.Diagnostics.Tracing; - using System.Linq; - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.CSharp; - using Microsoft.CodeAnalysis.Testing; - using Xunit; - using Verify = CSharpCodeFixVerifier; - - public class VSTHRD003UseJtfRunAsyncAnalyzerTests - { - [Fact] - public async Task ReportWarningWhenTaskIsDefinedOutsideDelegate() - { - var test = @" + [Fact] + public async Task ReportWarningWhenTaskIsDefinedOutsideDelegate() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -44,18 +35,18 @@ public async Task SomeOperationAsync() } } "; - DiagnosticResult[] expected = - { - Verify.Diagnostic().WithLocation(15, 19), - Verify.Diagnostic().WithLocation(16, 19), - }; - await Verify.VerifyAnalyzerAsync(test, expected); - } - - [Fact] - public async Task ReportWarningWhenTaskTIsDefinedOutsideDelegate() + DiagnosticResult[] expected = { - var test = @" + CSVerify.Diagnostic().WithLocation(15, 19), + CSVerify.Diagnostic().WithLocation(16, 19), + }; + await CSVerify.VerifyAnalyzerAsync(test, expected); + } + + [Fact] + public async Task ReportWarningWhenTaskTIsDefinedOutsideDelegate() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -80,14 +71,14 @@ public async Task SomeOperationAsync() } } "; - DiagnosticResult expected = Verify.Diagnostic().WithLocation(14, 19); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(14, 19); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task ReportWarningWhenTaskTIsReturnedDirectlyFromLambda() - { - var test = @" + [Fact] + public async Task ReportWarningWhenTaskTIsReturnedDirectlyFromLambda() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -100,14 +91,14 @@ public static T WaitAndGetResult(Task task) } } "; - DiagnosticResult expected = Verify.Diagnostic().WithLocation(10, 59); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(10, 59); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task ReportWarningWhenTaskTIsReturnedDirectlyFromDelegate() - { - var test = @" + [Fact] + public async Task ReportWarningWhenTaskTIsReturnedDirectlyFromDelegate() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -120,14 +111,14 @@ public static T WaitAndGetResult(Task task) } } "; - DiagnosticResult expected = Verify.Diagnostic().WithLocation(10, 68); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(10, 68); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task ReportWarningWhenTaskIsReturnedDirectlyFromMethod() - { - var test = @" + [Fact] + public async Task ReportWarningWhenTaskIsReturnedDirectlyFromMethod() + { + var test = @" using System.Threading.Tasks; class Tests @@ -140,14 +131,14 @@ public Task GetTask() } } "; - DiagnosticResult expected = this.CreateDiagnostic(10, 16, 4); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = this.CreateDiagnostic(10, 16, 4); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task ReportWarningWhenTaskIsReturnedDirectlyFromMethodViaExpressionBody() - { - var test = @" + [Fact] + public async Task ReportWarningWhenTaskIsReturnedDirectlyFromMethodViaExpressionBody() + { + var test = @" using System.Threading.Tasks; class Tests @@ -157,14 +148,14 @@ class Tests public Task GetTask() => task; } "; - DiagnosticResult expected = this.CreateDiagnostic(8, 30, 4); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = this.CreateDiagnostic(8, 30, 4); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task ReportWarningWhenTaskParameterIsReturnedDirectlyFromMethodViaExpressionBody() - { - var test = @" + [Fact] + public async Task ReportWarningWhenTaskParameterIsReturnedDirectlyFromMethodViaExpressionBody() + { + var test = @" using System.Threading.Tasks; class Tests @@ -172,14 +163,14 @@ class Tests public Task GetTask(Task task) => task; } "; - DiagnosticResult expected = this.CreateDiagnostic(6, 39, 4); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = this.CreateDiagnostic(6, 39, 4); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task ReportWarningWhenTaskIsReturnedAwaitedFromMethod() - { - var test = @" + [Fact] + public async Task ReportWarningWhenTaskIsReturnedAwaitedFromMethod() + { + var test = @" using System.Threading.Tasks; class Tests @@ -192,16 +183,16 @@ public async Task AwaitAndGetResult() } } "; - DiagnosticResult expected = this.CreateDiagnostic(10, 22, 4); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = this.CreateDiagnostic(10, 22, 4); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReportWarningWhenConfiguredTaskIsReturnedAwaitedFromMethod(bool continueOnCapturedContext) - { - var test = $@" + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReportWarningWhenConfiguredTaskIsReturnedAwaitedFromMethod(bool continueOnCapturedContext) + { + var test = $@" using System.Threading.Tasks; class Tests @@ -210,20 +201,19 @@ class Tests public async Task AwaitAndGetResult() {{ - await task.ConfigureAwait({(continueOnCapturedContext ? "true" : "false")}); + await [|task|].ConfigureAwait({(continueOnCapturedContext ? "true" : "false")}); }} }} "; - DiagnosticResult expected = this.CreateDiagnostic(10, 15, 21 + continueOnCapturedContext.ToString().Length); - await Verify.VerifyAnalyzerAsync(test, expected); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReportWarningWhenConfiguredTaskTIsReturnedAwaitedFromMethod(bool continueOnCapturedContext) - { - var test = $@" + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReportWarningWhenConfiguredTaskTIsReturnedAwaitedFromMethod(bool continueOnCapturedContext) + { + var test = $@" using System.Threading.Tasks; class Tests @@ -232,18 +222,17 @@ class Tests public async Task AwaitAndGetResult() {{ - return await task.ConfigureAwait({(continueOnCapturedContext ? "true" : "false")}); + return await [|task|].ConfigureAwait({(continueOnCapturedContext ? "true" : "false")}); }} }} "; - DiagnosticResult expected = this.CreateDiagnostic(10, 22, 21 + continueOnCapturedContext.ToString().Length); - await Verify.VerifyAnalyzerAsync(test, expected); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ReportWarningWhenConfiguredInlineTaskReturnedAwaitedFromMethod() - { - var test = @" + [Fact] + public async Task ReportWarningWhenConfiguredInlineTaskReturnedAwaitedFromMethod() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -253,18 +242,17 @@ class Tests public async Task AwaitAndGetResult() { - await task.ConfigureAwaitRunInline(); + await [|task|].ConfigureAwaitRunInline(); } } "; - DiagnosticResult expected = this.CreateDiagnostic(11, 15, 30); - await Verify.VerifyAnalyzerAsync(test, expected); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ReportWarningWhenConfiguredInlineTaskTReturnedAwaitedFromMethod() - { - var test = @" + [Fact] + public async Task ReportWarningWhenConfiguredInlineTaskTReturnedAwaitedFromMethod() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -274,18 +262,17 @@ class Tests public async Task AwaitAndGetResult() { - return await task.ConfigureAwaitRunInline(); + return await [|task|].ConfigureAwaitRunInline(); } } "; - DiagnosticResult expected = this.CreateDiagnostic(11, 22, 30); - await Verify.VerifyAnalyzerAsync(test, expected); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ReportWarningWhenTaskFromFieldIsAwaitedInJtfRunDelegate() - { - var test = @" + [Fact] + public async Task ReportWarningWhenTaskFromFieldIsAwaitedInJtfRunDelegate() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -303,14 +290,14 @@ static void Main(string[] args) } } "; - DiagnosticResult expected = this.CreateDiagnostic(14, 19, 1); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = this.CreateDiagnostic(14, 19, 1); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task ReportWarningWhenTaskTIsReturnedDirectlyWithCancellation() - { - var test = @" + [Fact] + public async Task ReportWarningWhenTaskTIsReturnedDirectlyWithCancellation() + { + var test = @" using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; @@ -324,14 +311,14 @@ public static T WaitAndGetResult(Task task, CancellationToken cancellation } } "; - DiagnosticResult expected = Verify.Diagnostic().WithLocation(11, 59); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(11, 59); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task DoNotReportWarningWhenTaskTIsPassedAsArgumentAndNoTaskIsReturned() - { - var test = @" + [Fact] + public async Task DoNotReportWarningWhenTaskTIsPassedAsArgumentAndNoTaskIsReturned() + { + var test = @" using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; @@ -348,13 +335,13 @@ public static int WaitAndGetResult(Task task) private static int DoSomethingWith(Task t) => 3; } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ReportWarningWhenTaskTIsPassedAsArgumentAndTaskIsReturned() - { - var test = @" + [Fact] + public async Task ReportWarningWhenTaskTIsPassedAsArgumentAndTaskIsReturned() + { + var test = @" using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; @@ -371,14 +358,14 @@ public static void WaitAndGetResult(Task task, CancellationToken cancellat private static Task DoSomethingWith(Task t) => null; } "; - DiagnosticResult expected = this.CreateDiagnostic(12, 68, 4); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = this.CreateDiagnostic(12, 68, 4); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task ReportWarningWhenTaskIsDefinedOutsideDelegateUsingRunAsync() - { - var test = @" + [Fact] + public async Task ReportWarningWhenTaskIsDefinedOutsideDelegateUsingRunAsync() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -403,14 +390,14 @@ public async Task SomeOperationAsync() } } "; - DiagnosticResult expected = Verify.Diagnostic().WithLocation(14, 19); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(14, 19); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task ReportWarningWhenTaskIsDefinedOutsideParanthesizedLambdaExpression() - { - var test = @" + [Fact] + public async Task ReportWarningWhenTaskIsDefinedOutsideParanthesizedLambdaExpression() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -436,14 +423,14 @@ public async Task SomeOperationAsync() } } "; - DiagnosticResult expected = Verify.Diagnostic().WithLocation(14, 19); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(14, 19); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task DoNotReportWarningWhenTaskIsDefinedWithinDelegate() - { - var test = @" + [Fact] + public async Task DoNotReportWarningWhenTaskIsDefinedWithinDelegate() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -470,13 +457,13 @@ public async Task SomeOperationAsync() } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task DoNotReportWarningWhenReturnedTaskIsDirectlyReturnedFromInvocation() - { - var test = @" + [Fact] + public async Task DoNotReportWarningWhenReturnedTaskIsDirectlyReturnedFromInvocation() + { + var test = @" using System.Threading.Tasks; class Tests @@ -489,13 +476,13 @@ public Task Test() public Task SomeOperationAsync() => Task.CompletedTask; } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task DoNotReportWarningWhenReturnedTaskIsAwaitedReturnedFromInvocation() - { - var test = @" + [Fact] + public async Task DoNotReportWarningWhenReturnedTaskIsAwaitedReturnedFromInvocation() + { + var test = @" using System.Threading.Tasks; class Tests @@ -508,13 +495,13 @@ public async Task Test() public Task SomeOperationAsync() => Task.FromResult(3); } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task DoNotReportWarningWhenTaskIsDefinedWithinDelegateInSubblock() - { - var test = @" + [Fact] + public async Task DoNotReportWarningWhenTaskIsDefinedWithinDelegateInSubblock() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -544,13 +531,13 @@ public async Task SomeOperationAsync() } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task DoNotReportWarningWhenTaskIsDefinedOutsideButInitializedWithinDelegate() - { - var test = @" + [Fact] + public async Task DoNotReportWarningWhenTaskIsDefinedOutsideButInitializedWithinDelegate() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -576,13 +563,13 @@ public async Task SomeOperationAsync() } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task DoNotReportWarningWhenTaskIsInitializedBothOutsideAndInsideDelegate() - { - var test = @" + [Fact] + public async Task DoNotReportWarningWhenTaskIsInitializedBothOutsideAndInsideDelegate() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -608,13 +595,13 @@ public async Task SomeOperationAsync() } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task DoNotReportWarningWhenTaskIsInitializedInsideDelegateConditionalStatement() - { - var test = @" + [Fact] + public async Task DoNotReportWarningWhenTaskIsInitializedInsideDelegateConditionalStatement() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -644,13 +631,13 @@ public async Task SomeOperationAsync() } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ReportWarningWhenTaskIsDefinedOutsideAndInitializedAfterAwait() - { - var test = @" + [Fact] + public async Task ReportWarningWhenTaskIsDefinedOutsideAndInitializedAfterAwait() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -676,14 +663,14 @@ public async Task SomeOperationAsync() } } "; - DiagnosticResult expected = Verify.Diagnostic().WithLocation(14, 19); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(14, 19); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task ReportWarningWhenTaskIsDefinedOutsideAndInitializationIsCommentedOut() - { - var test = @" + [Fact] + public async Task ReportWarningWhenTaskIsDefinedOutsideAndInitializationIsCommentedOut() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -710,14 +697,14 @@ public async Task SomeOperationAsync() } } "; - DiagnosticResult expected = Verify.Diagnostic().WithLocation(16, 19); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(16, 19); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task ReportWarningWhenAwaitIsInsideForLoop() - { - var test = @" + [Fact] + public async Task ReportWarningWhenAwaitIsInsideForLoop() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -745,14 +732,14 @@ public async Task SomeOperationAsync() } } "; - DiagnosticResult expected = Verify.Diagnostic().WithLocation(16, 23); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(16, 23); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task ReportWarningsForMultipleAwaits() - { - var test = @" + [Fact] + public async Task ReportWarningsForMultipleAwaits() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -779,20 +766,20 @@ public async Task SomeOperationAsync() } } "; - DiagnosticResult[] expected = - { - Verify.Diagnostic().WithLocation(14, 19), - Verify.Diagnostic().WithLocation(15, 19), - Verify.Diagnostic().WithLocation(16, 19), - }; + DiagnosticResult[] expected = + { + CSVerify.Diagnostic().WithLocation(14, 19), + CSVerify.Diagnostic().WithLocation(15, 19), + CSVerify.Diagnostic().WithLocation(16, 19), + }; - await Verify.VerifyAnalyzerAsync(test, expected); - } + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task DoNotReportWarningWhenAwaitingAsyncMethod() - { - var test = @" + [Fact] + public async Task DoNotReportWarningWhenAwaitingAsyncMethod() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -816,13 +803,13 @@ public async Task SomeOperationAsync() } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task DoNotReportWarningWhenAwaitingJoinableTaskDefinedInsideDelegate() - { - var test = @" + [Fact] + public async Task DoNotReportWarningWhenAwaitingJoinableTaskDefinedInsideDelegate() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -844,13 +831,13 @@ public void Test() } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task DoNotReportWarningWhenAwaitingJoinableTaskDefinedOutsideDelegate() - { - var test = @" + [Fact] + public async Task DoNotReportWarningWhenAwaitingJoinableTaskDefinedOutsideDelegate() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -873,13 +860,13 @@ public void Test() } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ReportWarningWhenHavingNestedLambdaExpressions() - { - var test = @" + [Fact] + public async Task ReportWarningWhenHavingNestedLambdaExpressions() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -889,7 +876,7 @@ class Tests public void Test() { JoinableTaskFactory jtf = ThreadHelper.JoinableTaskFactory; - + jtf.Run(async () => { System.Threading.Tasks.Task task = SomeOperationAsync(); @@ -909,14 +896,14 @@ public async Task SomeOperationAsync() } } "; - DiagnosticResult expected = Verify.Diagnostic().WithLocation(17, 23); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(17, 23); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task ReportWarningForDerivedJoinableTaskFactory() - { - var test = @" + [Fact] + public async Task ReportWarningForDerivedJoinableTaskFactory() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -951,14 +938,14 @@ public async Task SomeOperationAsync() } } "; - DiagnosticResult expected = Verify.Diagnostic().WithLocation(24, 19); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(24, 19); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task ReportWarningWhenAwaitingTaskInField() - { - var test = @" + [Fact] + public async Task ReportWarningWhenAwaitingTaskInField() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -974,14 +961,14 @@ public void Test() { } } "; - DiagnosticResult expected = this.CreateDiagnostic(12, 19, 4); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = this.CreateDiagnostic(12, 19, 4); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task ReportWarningWhenAwaitingTaskInField_WithThisQualifier() - { - var test = @" + [Fact] + public async Task ReportWarningWhenAwaitingTaskInField_WithThisQualifier() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -997,14 +984,14 @@ public void Test() { } } "; - DiagnosticResult expected = this.CreateDiagnostic(12, 19, 9); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = this.CreateDiagnostic(12, 19, 9); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task DoNotReportWarningWhenAwaitingTaskInFieldThatIsAssignedLocally() - { - var test = @" + [Fact] + public async Task DoNotReportWarningWhenAwaitingTaskInFieldThatIsAssignedLocally() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -1023,13 +1010,13 @@ public void Test() { Task SomeOperationAsync() => Task.CompletedTask; } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task DoNotReportWarningWhenCompletedTaskIsReturnedDirectlyFromMethod() - { - var test = @" + [Fact] + public async Task DoNotReportWarningWhenCompletedTaskIsReturnedDirectlyFromMethod() + { + var test = @" using System; using System.Threading; using System.Threading.Tasks; @@ -1068,13 +1055,13 @@ public Task GetTask(int i) } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task DoNotReportWarningWhenTaskFromResultIsReturnedDirectlyFromMethod() - { - var test = @" + [Fact] + public async Task DoNotReportWarningWhenTaskFromResultIsReturnedDirectlyFromMethod() + { + var test = @" using System.Threading.Tasks; class Tests @@ -1085,13 +1072,13 @@ public Task GetTask() } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task DoNotReportWarningWhenTaskFromResultIsReturnedDirectlyFromMethod_FromField() - { - var test = @" + [Fact] + public async Task DoNotReportWarningWhenTaskFromResultIsReturnedDirectlyFromMethod_FromField() + { + var test = @" using System.Threading.Tasks; class Tests @@ -1104,13 +1091,13 @@ public Task GetTask() } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ReportWarningWhenTaskFromResultIsReturnedDirectlyFromMethod_FromField_NotReadOnly() - { - var test = @" + [Fact] + public async Task ReportWarningWhenTaskFromResultIsReturnedDirectlyFromMethod_FromField_NotReadOnly() + { + var test = @" using System.Threading.Tasks; class Tests @@ -1123,14 +1110,14 @@ public Task GetTask() } } "; - DiagnosticResult expected = this.CreateDiagnostic(10, 16, 13); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = this.CreateDiagnostic(10, 16, 13); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task ReportWarningWhenTaskRunIsReturnedDirectlyFromMethod_FromField() - { - var test = @" + [Fact] + public async Task ReportWarningWhenTaskRunIsReturnedDirectlyFromMethod_FromField() + { + var test = @" using System.Threading.Tasks; class Tests @@ -1143,16 +1130,16 @@ public Task GetTask() } } "; - DiagnosticResult expected = this.CreateDiagnostic(10, 16, 8); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = this.CreateDiagnostic(10, 16, 8); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task TaskReturningMethodIncludeArgumentFromOtherSyntaxTree() - { - // This is a regression test for a bug that only repro'd when the field was defined in a different document from where it was used - // as input to a return value from a Task-returning method. - var source1 = @" + [Fact] + public async Task TaskReturningMethodIncludeArgumentFromOtherSyntaxTree() + { + // This is a regression test for a bug that only repro'd when the field was defined in a different document from where it was used + // as input to a return value from a Task-returning method. + var source1 = @" using System.Collections.Immutable; using System.Threading.Tasks; @@ -1164,7 +1151,7 @@ private Task> SomethingAsync() } } "; - var source2 = @" + var source2 = @" using System.Collections.Immutable; class OtherClass @@ -1177,14 +1164,14 @@ class OtherClass } "; - var test = new Verify.Test { TestState = { Sources = { source1, source2 } } }; - await test.RunAsync(); - } + var test = new CSVerify.Test { TestState = { Sources = { source1, source2 } } }; + await test.RunAsync(); + } - [Fact] - public async Task CachedTaskReturnedFromExternalToCompilation() - { - string specialTasksCs = @" + [Fact] + public async Task CachedTaskReturnedFromExternalToCompilation() + { + string specialTasksCs = @" using System.Threading.Tasks; public static class SpecialTasks { @@ -1192,14 +1179,14 @@ public static class SpecialTasks { } "; - Verify.Test? test = null; - test = new Verify.Test + CSVerify.Test? test = null; + test = new CSVerify.Test + { + TestState = { - TestState = + Sources = { - Sources = - { - @" + @" using System.Threading.Tasks; public static class Boom { @@ -1209,32 +1196,32 @@ static Task MyMethodAsync() } } ", - }, - AdditionalProjects = + }, + AdditionalProjects = + { + ["ProjectA"] = { - ["ProjectA"] = + Sources = { - Sources = - { - ("SpecialTasks.cs", specialTasksCs), - }, + ("SpecialTasks.cs", specialTasksCs), }, }, - AdditionalProjectReferences = - { - "ProjectA", - }, }, - }; + AdditionalProjectReferences = + { + "ProjectA", + }, + }, + }; - await test.RunAsync(); - } + await test.RunAsync(); + } - [Fact] - public async Task DoNotReportWarningWithParenthesizedAwaitExpressions() - { - // This is a test for bug 849. Parenthesized expressions caused an InvalidCastException. - var test = @" + [Fact] + public async Task DoNotReportWarningWithParenthesizedAwaitExpressions() + { + // This is a test for bug 849. Parenthesized expressions caused an InvalidCastException. + var test = @" using System.Threading.Tasks; class Test { @@ -1249,10 +1236,223 @@ async Task FooAsync() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - private DiagnosticResult CreateDiagnostic(int line, int column, int length) => - Verify.Diagnostic().WithSpan(line, column, line, column + length); + [Fact] + public async Task ReportWarningWhenAwaitingTaskReturningProperty() + { + var test = @" +using System.Threading.Tasks; + +class Tests +{ + async Task GetTask(TaskCompletionSource tcs) + { + await [|tcs.Task|]; } } +"; + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task DoNotReportWarningWhenAwaitingTaskPropertyThatWasSetInContext() + { + var test = @" +using System.Threading.Tasks; + +class Tests +{ + private Task MyTaskProperty { get; set; } + + async Task GetTask() + { + this.MyTaskProperty = Task.Run(() => {}); + await this.MyTaskProperty; + } +} +"; + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task DoNotReportWarningWhenAwaitingTaskPropertyOfObjectCreatedInContext() + { + var test = @" +using System.Threading.Tasks; + +class Tests +{ + private Task MyTaskProperty { get; set; } + + static async Task GetTask() + { + // our own property. + var obj = new Tests(); + await obj.MyTaskProperty; + + // local with initializer + var tcs = new TaskCompletionSource(); + await tcs.Task; + + // Assign later + TaskCompletionSource tcs2; + tcs2 = new TaskCompletionSource(); + await tcs2.Task; + + // Assigned, but not to a newly created object. + TaskCompletionSource tcs3 = tcs2; + await [|tcs3.Task|]; + } +} +"; + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task DoNotReportWarningWhenAwaitingTaskPropertyOfObjectCreatedInContext_TargetTypeCreation() + { + string test = """ + using System.Threading.Tasks; + + class Test + { + static Task Exec2Async(string executable, params string[] args) + { + Process p = new(); + return p.Task; + } + } + + class Process + { + public Task Task { get; } + } + """; + await CSVerify.VerifyAnalyzerAsync(test); + } + + /// + /// This is important to allow folks to return jtf.RunAsync(...).Task from a method. + /// + [Fact] + public async Task DoNotReportWarningWhenAwaitingTaskPropertyOfObjectReturnedFromMethod() + { + var test = @" +using System.Threading.Tasks; + +class Tests +{ + private Task MyTaskProperty { get; set; } + + static Tests NewTests() => new Tests(); + + static async Task GetTask() + { + await NewTests().MyTaskProperty; + } +} +"; + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task DoNotReportWarningWhenAwaitingTaskPropertyOfObjectReturnedFromMethodViaLocal() + { + var test = """ + using System.Threading.Tasks; + + class JsonRpc + { + internal static JsonRpc Attach() => throw new System.NotImplementedException(); + + internal Task Completion { get; } + } + + class Tests + { + static async Task ListenAndWait() + { + var jsonRpc = JsonRpc.Attach(); + await jsonRpc.Completion; + } + } + """; + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task DoNotReportWarningWhenAwaitingTaskPropertyOfObjectReturnedFromAsyncMethodViaLocal() + { + var test = """ + using System.Threading.Tasks; + + class JsonRpc + { + internal static Task AttachAsync() => throw new System.NotImplementedException(); + + internal Task Completion { get; } + } + + class Tests + { + static async Task ListenAndWait() + { + var jsonRpc = await JsonRpc.AttachAsync(); + await jsonRpc.Completion; + } + } + """; + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task ReportWarningWhenAwaitingTaskPropertyThatWasNotSetInContext() + { + var test = @" +using System.Threading.Tasks; + +class Tests +{ + private Task MyTaskProperty { get; set; } = Task.Run(() => {}); + + async Task GetTask() + { + await [|this.MyTaskProperty|]; + await [|MyTaskProperty|]; + } +} +"; + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task DoNotReportWarningWhenReturningTaskFromLambdaArgument() + { + var test = """ + using System.Linq; + using System.Threading.Tasks; + + class JsonRpc + { + internal static JsonRpc Attach() => throw new System.NotImplementedException(); + + internal Task Completion { get; } + } + + class Tests + { + static async Task ListenAndWait() + { + JsonRpc[] rpcs = new [] { JsonRpc.Attach(), JsonRpc.Attach() }; + await Task.WhenAll(rpcs.Select(r => r.Completion)); + } + } + """; + await CSVerify.VerifyAnalyzerAsync(test); + } + + private DiagnosticResult CreateDiagnostic(int line, int column, int length) => + CSVerify.Diagnostic().WithSpan(line, column, line, column + length); +} diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD004AwaitSwitchToMainThreadAsyncAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD004AwaitSwitchToMainThreadAsyncAnalyzerTests.cs index 599c1bff7..54483b7d9 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD004AwaitSwitchToMainThreadAsyncAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD004AwaitSwitchToMainThreadAsyncAnalyzerTests.cs @@ -1,19 +1,14 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System.Threading.Tasks; - using Microsoft.CodeAnalysis.Testing; - using Xunit; - using Verify = CSharpCodeFixVerifier; +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; - public class VSTHRD004AwaitSwitchToMainThreadAsyncAnalyzerTests +public class VSTHRD004AwaitSwitchToMainThreadAsyncAnalyzerTests +{ + [Fact] + public async Task SyncMethod_ProducesDiagnostic() { - [Fact] - public async Task SyncMethod_ProducesDiagnostic() - { - var test = @" + var test = @" class Test { Microsoft.VisualStudio.Threading.JoinableTaskFactory jtf; @@ -25,13 +20,13 @@ void Foo() } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task AsyncMethod_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task AsyncMethod_ProducesDiagnostic() + { + var test = @" using System.Threading.Tasks; class Test @@ -46,13 +41,13 @@ async Task FooAsync() } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task AsyncMethod_NoAwaitInParenthesizedLambda_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task AsyncMethod_NoAwaitInParenthesizedLambda_ProducesDiagnostic() + { + var test = @" using System.Threading.Tasks; class Test @@ -66,13 +61,13 @@ async Task FooAsync() } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task AsyncMethod_NoAwaitInAnonymousDelegate_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task AsyncMethod_NoAwaitInAnonymousDelegate_ProducesDiagnostic() + { + var test = @" using System.Threading.Tasks; class Test @@ -86,13 +81,13 @@ async Task FooAsync() } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task AsyncMethodWithAwait_ProducesNoDiagnostic() - { - var test = @" + [Fact] + public async Task AsyncMethodWithAwait_ProducesNoDiagnostic() + { + var test = @" using System.Threading.Tasks; class Test @@ -106,13 +101,33 @@ async Task FooAsync() } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task AsyncMethodWithAwaitNoThrowAwaitable_ProducesNoDiagnostic() + { + var test = @" +using System.Threading.Tasks; - [Fact] - public async Task TaskReturningSyncMethod_ProducesDiagnostic() - { - var test = @" +class Test +{ + Microsoft.VisualStudio.Threading.JoinableTaskFactory jtf; + + async Task FooAsync() + { + await jtf.SwitchToMainThreadAsync().NoThrowAwaitable(); + } +} +"; + + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task TaskReturningSyncMethod_ProducesDiagnostic() + { + var test = @" using System.Threading.Tasks; class Test @@ -127,7 +142,6 @@ Task FooAsync() } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD010MainThreadUsageAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD010MainThreadUsageAnalyzerTests.cs index 975851ea2..a8a74b77b 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD010MainThreadUsageAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD010MainThreadUsageAnalyzerTests.cs @@ -1,20 +1,15 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System.Threading.Tasks; - using Microsoft.CodeAnalysis.Testing; - using Xunit; - using static Microsoft.VisualStudio.Threading.Analyzers.VSTHRD010MainThreadUsageAnalyzer; - using Verify = CSharpCodeFixVerifier; +using static Microsoft.VisualStudio.Threading.Analyzers.VSTHRD010MainThreadUsageAnalyzer; +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; - public class VSTHRD010MainThreadUsageAnalyzerTests +public class VSTHRD010MainThreadUsageAnalyzerTests +{ + [Fact] + public async Task InvokeVsReferenceOutsideMethod() { - [Fact] - public async Task InvokeVsReferenceOutsideMethod() - { - var test = @" + var test = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -26,14 +21,14 @@ class Test { string name = G.Ref1.Name; } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithSpan(10, 26, 10, 30).WithArguments("IVsReference", "Test.VerifyOnUIThread"); - await Verify.VerifyCodeFixAsync(test, expected, test); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithSpan(10, 26, 10, 30).WithArguments("IVsReference", "Test.VerifyOnUIThread"); + await CSVerify.VerifyCodeFixAsync(test, expected, test); + } - [Fact] - public async Task InvokeVsSolutionComplexStyle() - { - var test = @" + [Fact] + public async Task InvokeVsSolutionComplexStyle() + { + var test = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -45,7 +40,7 @@ void F() { IVsSolution Method() { return null; } } "; - var fix = @" + var fix = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -58,19 +53,19 @@ void F() { IVsSolution Method() { return null; } } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithSpan(7, 23, 7, 34).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); - await new Verify.Test - { - TestCode = test, - ExpectedDiagnostics = { expected }, - FixedCode = fix, - }.RunAsync(); - } - - [Fact] - public async Task InvokeVsSolutionNoCheck() + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithSpan(7, 23, 7, 34).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); + await new CSVerify.Test { - var test = @" + TestCode = test, + ExpectedDiagnostics = { expected }, + FixedCode = fix, + }.RunAsync(); + } + + [Fact] + public async Task InvokeVsSolutionNoCheck() + { + var test = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -81,7 +76,7 @@ void F() { } } "; - var fix = @" + var fix = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -93,26 +88,26 @@ void F() { } } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithSpan(8, 13, 8, 24).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); - await new Verify.Test - { - TestCode = test, - ExpectedDiagnostics = { expected }, - FixedCode = fix, - }.RunAsync(); - } - - /// - /// Describes an idea for how another code fix can offer to wrap a method in a JTF.Run delegate to switch to the main thread. - /// - /// - /// This will need much more thorough testing than just this method, when the feature is implemented. - /// There are ref and out parameters, and return values to consider, for example. - /// - [Fact(Skip = "Feature is not yet implemented.")] - public async Task InvokeVsSolutionNoCheck_FixByJTFRunAndSwitch() + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithSpan(8, 13, 8, 24).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); + await new CSVerify.Test { - var test = @" + TestCode = test, + ExpectedDiagnostics = { expected }, + FixedCode = fix, + }.RunAsync(); + } + + /// + /// Describes an idea for how another code fix can offer to wrap a method in a JTF.Run delegate to switch to the main thread. + /// + /// + /// This will need much more thorough testing than just this method, when the feature is implemented. + /// There are ref and out parameters, and return values to consider, for example. + /// + [Fact(Skip = "Feature is not yet implemented.")] + public async Task InvokeVsSolutionNoCheck_FixByJTFRunAndSwitch() + { + var test = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -123,7 +118,7 @@ void F() { } } "; - var fix = @" + var fix = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -137,19 +132,19 @@ void F() { } } "; - await new Verify.Test - { - TestCode = test, - ExpectedDiagnostics = { Verify.Diagnostic(DescriptorSync).WithSpan(8, 13, 8, 24).WithArguments("IVsSolution", "Test.VerifyOnUIThread") }, - FixedCode = fix, - CodeActionIndex = CodeFixIndex.SwitchToMainThreadAsync, - }.RunAsync(); - } - - [Fact] - public async Task InvokeVsSolutionNoCheck_InProperty() + await new CSVerify.Test { - var test = @" + TestCode = test, + ExpectedDiagnostics = { CSVerify.Diagnostic(DescriptorSync).WithSpan(8, 13, 8, 24).WithArguments("IVsSolution", "Test.VerifyOnUIThread") }, + FixedCode = fix, + CodeActionIndex = CodeFixIndex.SwitchToMainThreadAsync, + }.RunAsync(); + } + + [Fact] + public async Task InvokeVsSolutionNoCheck_InProperty() + { + var test = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -163,7 +158,7 @@ int F { } } "; - var fix = @" + var fix = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -178,19 +173,19 @@ int F { } } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithSpan(9, 17, 9, 28).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); - await new Verify.Test - { - TestCode = test, - ExpectedDiagnostics = { expected }, - FixedCode = fix, - }.RunAsync(); - } - - [Fact] - public async Task InvokeVsSolutionNoCheck_InCtor() + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithSpan(9, 17, 9, 28).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); + await new CSVerify.Test { - var test = @" + TestCode = test, + ExpectedDiagnostics = { expected }, + FixedCode = fix, + }.RunAsync(); + } + + [Fact] + public async Task InvokeVsSolutionNoCheck_InCtor() + { + var test = @" using Microsoft.VisualStudio.Shell.Interop; class Test { @@ -200,7 +195,7 @@ class Test { } } "; - var fix = @" + var fix = @" using Microsoft.VisualStudio.Shell.Interop; class Test { @@ -211,19 +206,19 @@ class Test { } } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithSpan(7, 13, 7, 24).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); - await new Verify.Test - { - TestCode = test, - ExpectedDiagnostics = { expected }, - FixedCode = fix, - }.RunAsync(); - } - - [Fact] - public async Task TransitiveNoCheck_InCtor() + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithSpan(7, 13, 7, 24).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); + await new CSVerify.Test { - var test = @" + TestCode = test, + ExpectedDiagnostics = { expected }, + FixedCode = fix, + }.RunAsync(); + } + + [Fact] + public async Task TransitiveNoCheck_InCtor() + { + var test = @" using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; @@ -242,7 +237,7 @@ static void VerifyOnUIThread() { } } "; - var fix1 = @" + var fix1 = @" using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; @@ -262,7 +257,7 @@ static void VerifyOnUIThread() { } } "; - var fix2 = @" + var fix2 = @" using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; @@ -283,28 +278,36 @@ static void VerifyOnUIThread() { } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithSpan(7, 9, 7, 12).WithArguments("Test.Foo", "Test.VerifyOnUIThread"); - await new Verify.Test - { - TestCode = test, - ExpectedDiagnostics = { expected }, - FixedCode = fix1, - CodeActionIndex = CodeFixIndex.VerifyOnUIThread, - }.RunAsync(); + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithSpan(7, 9, 7, 12).WithArguments("Test.Foo", "Test.VerifyOnUIThread"); + await new CSVerify.Test + { + TestCode = test, + ExpectedDiagnostics = { expected }, + FixedCode = fix1, + CodeActionIndex = CodeFixIndex.VerifyOnUIThread, - await new Verify.Test - { - TestCode = test, - ExpectedDiagnostics = { expected }, - FixedCode = fix2, - CodeActionIndex = CodeFixIndex.ThrowIfNotOnUIThreadIndex1, - }.RunAsync(); - } + // SkipLocalDiagnosticCheck is required because this diagnostic is reported at compilation-end + // (as a transitive/indirect diagnostic), not as a local one. See https://github.com/microsoft/vs-threading/issues/1364. + CodeFixTestBehaviors = CodeFixTestBehaviors.SkipLocalDiagnosticCheck, + }.RunAsync(); - [Fact] - public async Task InvokeVsSolutionWithCheck_InCtor() + await new CSVerify.Test { - var test = @" + TestCode = test, + ExpectedDiagnostics = { expected }, + FixedCode = fix2, + CodeActionIndex = CodeFixIndex.ThrowIfNotOnUIThreadIndex1, + + // SkipLocalDiagnosticCheck is required because this diagnostic is reported at compilation-end + // (as a transitive/indirect diagnostic), not as a local one. See https://github.com/microsoft/vs-threading/issues/1364. + CodeFixTestBehaviors = CodeFixTestBehaviors.SkipLocalDiagnosticCheck, + }.RunAsync(); + } + + [Fact] + public async Task InvokeVsSolutionWithCheck_InCtor() + { + var test = @" using Microsoft.VisualStudio.Shell.Interop; class Test { @@ -318,13 +321,13 @@ void VerifyOnUIThread() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task InvokeVsSolutionBeforeAndAfterVerifyOnUIThread() - { - var test = @" + [Fact] + public async Task InvokeVsSolutionBeforeAndAfterVerifyOnUIThread() + { + var test = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -340,7 +343,7 @@ void VerifyOnUIThread() { } } "; - var fix = @" + var fix = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -357,19 +360,19 @@ void VerifyOnUIThread() { } } "; - await new Verify.Test - { - TestCode = test, - ExpectedDiagnostics = { Verify.Diagnostic(DescriptorSync).WithSpan(8, 13, 8, 24).WithArguments("IVsSolution", "Test.VerifyOnUIThread") }, - FixedCode = fix, - CodeActionIndex = CodeFixIndex.ThrowIfNotOnUIThreadIndex0, - }.RunAsync(); - } - - [Fact(Skip = "Not yet supported. See https://github.com/Microsoft/vs-threading/issues/38")] - public async Task InvokeVsSolutionAfterConditionedVerifyOnUIThread() + await new CSVerify.Test { - var test = @" + TestCode = test, + ExpectedDiagnostics = { CSVerify.Diagnostic(DescriptorSync).WithSpan(8, 13, 8, 24).WithArguments("IVsSolution", "Test.VerifyOnUIThread") }, + FixedCode = fix, + CodeActionIndex = CodeFixIndex.ThrowIfNotOnUIThreadIndex0, + }.RunAsync(); + } + + [Fact(Skip = "Not yet supported. See https://github.com/Microsoft/vs-threading/issues/38")] + public async Task InvokeVsSolutionAfterConditionedVerifyOnUIThread() + { + var test = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -387,14 +390,14 @@ void VerifyOnUIThread() { } } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithSpan(12, 13, 12, 24).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithSpan(12, 13, 12, 24).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact(Skip = "Not yet supported. See https://github.com/Microsoft/vs-threading/issues/38")] - public async Task InvokeVsSolutionInBlockWithoutVerifyOnUIThread() - { - var test = @" + [Fact(Skip = "Not yet supported. See https://github.com/Microsoft/vs-threading/issues/38")] + public async Task InvokeVsSolutionInBlockWithoutVerifyOnUIThread() + { + var test = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -412,14 +415,14 @@ void VerifyOnUIThread() { } } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithSpan(11, 17, 11, 28).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithSpan(11, 17, 11, 28).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact(Skip = "Not yet supported. See https://github.com/Microsoft/vs-threading/issues/38")] - public async Task InvokeVsSolutionAfterSwallowingCatchBlockWhereVerifyOnUIThreadWasInTry() - { - var test = @" + [Fact(Skip = "Not yet supported. See https://github.com/Microsoft/vs-threading/issues/38")] + public async Task InvokeVsSolutionAfterSwallowingCatchBlockWhereVerifyOnUIThreadWasInTry() + { + var test = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -437,14 +440,14 @@ void VerifyOnUIThread() { } } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithSpan(12, 13, 12, 24).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithSpan(12, 13, 12, 24).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact(Skip = "Not yet supported. See https://github.com/microsoft/vs-threading/issues/542")] - public async Task InvokeVsSolutionAfterUIThreadAssertionAndSwitchToThreadPool() - { - var test = @" + [Fact(Skip = "Not yet supported. See https://github.com/microsoft/vs-threading/issues/542")] + public async Task InvokeVsSolutionAfterUIThreadAssertionAndSwitchToThreadPool() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell.Interop; @@ -464,14 +467,14 @@ void VerifyOnUIThread() { } } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorAsync).WithSpan(14, 13, 14, 24).WithArguments("IVsSolution", "JoinableTaskFactory.SwitchToMainThreadAsync"); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorAsync).WithSpan(14, 13, 14, 24).WithArguments("IVsSolution", "JoinableTaskFactory.SwitchToMainThreadAsync"); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact(Skip = "Not yet supported. See https://github.com/microsoft/vs-threading/issues/542")] - public async Task InvokeVsSolutionAfterUIThreadAssertionAndConfigureAwaitFalse() - { - var test = @" + [Fact(Skip = "Not yet supported. See https://github.com/microsoft/vs-threading/issues/542")] + public async Task InvokeVsSolutionAfterUIThreadAssertionAndConfigureAwaitFalse() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell.Interop; @@ -493,14 +496,14 @@ void VerifyOnUIThread() { async Task SomeAsync() => await Task.Yield(); } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorAsync).WithSpan(14, 13, 14, 24).WithArguments("IVsSolution", "JoinableTaskFactory.SwitchToMainThreadAsync"); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorAsync).WithSpan(14, 13, 14, 24).WithArguments("IVsSolution", "JoinableTaskFactory.SwitchToMainThreadAsync"); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact(Skip = "Not yet supported. See https://github.com/Microsoft/vs-threading/issues/38")] - public async Task InvokeVsSolutionAfterUIThreadAssertionAndConditionalSwitchToThreadPool() - { - var test = @" + [Fact(Skip = "Not yet supported. See https://github.com/Microsoft/vs-threading/issues/38")] + public async Task InvokeVsSolutionAfterUIThreadAssertionAndConditionalSwitchToThreadPool() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell.Interop; @@ -523,14 +526,14 @@ void VerifyOnUIThread() { } } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorAsync).WithSpan(16, 13, 16, 24).WithArguments("IVsSolution", "JoinableTaskFactory.SwitchToMainThreadAsync"); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorAsync).WithSpan(16, 13, 16, 24).WithArguments("IVsSolution", "JoinableTaskFactory.SwitchToMainThreadAsync"); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task RequiresUIThreadTransitive_MultipleInMember() - { - var test = @" + [Fact] + public async Task RequiresUIThreadTransitive_MultipleInMember() + { + var test = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -555,7 +558,7 @@ void H() { static void VerifyOnUIThread() { } } "; - var fix = @" + var fix = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -582,23 +585,27 @@ static void VerifyOnUIThread() { } } "; - await new Verify.Test + await new CSVerify.Test + { + TestCode = test, + ExpectedDiagnostics = { - TestCode = test, - ExpectedDiagnostics = - { - Verify.Diagnostic(DescriptorSync).WithSpan(19, 9, 19, 10).WithArguments("Test.F", "Test.VerifyOnUIThread"), - Verify.Diagnostic(DescriptorSync).WithSpan(20, 9, 20, 10).WithArguments("Test.G", "Test.VerifyOnUIThread"), - }, - FixedCode = fix, - CodeActionIndex = CodeFixIndex.VerifyOnUIThread, - }.RunAsync(); - } + CSVerify.Diagnostic(DescriptorSync).WithSpan(19, 9, 19, 10).WithArguments("Test.F", "Test.VerifyOnUIThread"), + CSVerify.Diagnostic(DescriptorSync).WithSpan(20, 9, 20, 10).WithArguments("Test.G", "Test.VerifyOnUIThread"), + }, + FixedCode = fix, + CodeActionIndex = CodeFixIndex.VerifyOnUIThread, - [Fact] - public async Task RequiresUIThreadTransitive() - { - var test = @" + // SkipLocalDiagnosticCheck is required because this diagnostic is reported at compilation-end + // (as a transitive/indirect diagnostic), not as a local one. See https://github.com/microsoft/vs-threading/issues/1364. + CodeFixTestBehaviors = CodeFixTestBehaviors.SkipLocalDiagnosticCheck, + }.RunAsync(); + } + + [Fact] + public async Task RequiresUIThreadTransitive() + { + var test = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -656,27 +663,27 @@ void VerifyOnUIThread() { } } "; - var expected = new DiagnosticResult[] - { - Verify.Diagnostic(DescriptorSync).WithSpan(13, 9, 13, 10).WithArguments("Test.F", "Test.VerifyOnUIThread"), - Verify.Diagnostic(DescriptorSync).WithSpan(17, 9, 17, 10).WithArguments("Test.G", "Test.VerifyOnUIThread"), - Verify.Diagnostic(DescriptorSync).WithSpan(22, 13, 22, 14).WithArguments("Test.H", "Test.VerifyOnUIThread"), - Verify.Diagnostic(DescriptorSync).WithSpan(32, 16, 32, 17).WithArguments("Test.H", "Test.VerifyOnUIThread"), - Verify.Diagnostic(DescriptorSync).WithSpan(35, 39, 35, 55).WithArguments("Test.get_MainThreadGetter", "Test.VerifyOnUIThread"), - Verify.Diagnostic(DescriptorSync).WithSpan(36, 40, 36, 61).WithArguments("Test.get_MainThreadGetter", "Test.VerifyOnUIThread"), - Verify.Diagnostic(DescriptorSync).WithSpan(37, 40, 37, 69).WithArguments("Test.get_MainThreadGetter", "Test.VerifyOnUIThread"), - Verify.Diagnostic(DescriptorSync).WithSpan(43, 39, 43, 55).WithArguments("Test.set_MainThreadSetter", "Test.VerifyOnUIThread"), - Verify.Diagnostic(DescriptorSync).WithSpan(44, 40, 44, 61).WithArguments("Test.set_MainThreadSetter", "Test.VerifyOnUIThread"), - Verify.Diagnostic(DescriptorSync).WithSpan(45, 40, 45, 69).WithArguments("Test.set_MainThreadSetter", "Test.VerifyOnUIThread"), - }; - - await Verify.VerifyAnalyzerAsync(test, expected); - } - - [Fact] - public async Task RequiresUIThreadNotTransitiveIfNotExplicit() + var expected = new DiagnosticResult[] { - var test = @" + CSVerify.Diagnostic(DescriptorSync).WithSpan(13, 9, 13, 10).WithArguments("Test.F", "Test.VerifyOnUIThread"), + CSVerify.Diagnostic(DescriptorSync).WithSpan(17, 9, 17, 10).WithArguments("Test.G", "Test.VerifyOnUIThread"), + CSVerify.Diagnostic(DescriptorSync).WithSpan(22, 13, 22, 14).WithArguments("Test.H", "Test.VerifyOnUIThread"), + CSVerify.Diagnostic(DescriptorSync).WithSpan(32, 16, 32, 17).WithArguments("Test.H", "Test.VerifyOnUIThread"), + CSVerify.Diagnostic(DescriptorSync).WithSpan(35, 39, 35, 55).WithArguments("Test.get_MainThreadGetter", "Test.VerifyOnUIThread"), + CSVerify.Diagnostic(DescriptorSync).WithSpan(36, 40, 36, 61).WithArguments("Test.get_MainThreadGetter", "Test.VerifyOnUIThread"), + CSVerify.Diagnostic(DescriptorSync).WithSpan(37, 40, 37, 69).WithArguments("Test.get_MainThreadGetter", "Test.VerifyOnUIThread"), + CSVerify.Diagnostic(DescriptorSync).WithSpan(43, 39, 43, 55).WithArguments("Test.set_MainThreadSetter", "Test.VerifyOnUIThread"), + CSVerify.Diagnostic(DescriptorSync).WithSpan(44, 40, 44, 61).WithArguments("Test.set_MainThreadSetter", "Test.VerifyOnUIThread"), + CSVerify.Diagnostic(DescriptorSync).WithSpan(45, 40, 45, 69).WithArguments("Test.set_MainThreadSetter", "Test.VerifyOnUIThread"), + }; + + await CSVerify.VerifyAnalyzerAsync(test, expected); + } + + [Fact] + public async Task RequiresUIThreadNotTransitiveIfNotExplicit() + { + var test = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -698,14 +705,14 @@ void VerifyOnUIThread() { } } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithSpan(8, 13, 8, 24).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithSpan(8, 13, 8, 24).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task RequiresUIThread_NotTransitiveThroughAsyncCalls() - { - var test = @" + [Fact] + public async Task RequiresUIThread_NotTransitiveThroughAsyncCalls() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -725,13 +732,13 @@ private async Task FooAsync() { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task InvokeVsSolutionAfterSwitchedToMainThreadAsync() - { - var test = @" + [Fact] + public async Task InvokeVsSolutionAfterSwitchedToMainThreadAsync() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell.Interop; @@ -747,13 +754,13 @@ async Task F() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task InvokeVsSolutionInLambda() - { - var test = @" + [Fact] + public async Task InvokeVsSolutionInLambda() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell.Interop; @@ -771,7 +778,7 @@ static void VerifyOnUIThread() { } } "; - var fix = @" + var fix = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell.Interop; @@ -790,19 +797,19 @@ static void VerifyOnUIThread() { } } "; - await new Verify.Test - { - TestCode = test, - ExpectedDiagnostics = { Verify.Diagnostic(DescriptorSync).WithSpan(11, 17, 11, 28).WithArguments("IVsSolution", "Test.VerifyOnUIThread") }, - FixedCode = fix, - CodeActionIndex = CodeFixIndex.VerifyOnUIThread, - }.RunAsync(); - } - - [Fact] - public async Task InvokeVsSolutionInSimpleLambda() + await new CSVerify.Test { - var test = @" + TestCode = test, + ExpectedDiagnostics = { CSVerify.Diagnostic(DescriptorSync).WithSpan(11, 17, 11, 28).WithArguments("IVsSolution", "Test.VerifyOnUIThread") }, + FixedCode = fix, + CodeActionIndex = CodeFixIndex.VerifyOnUIThread, + }.RunAsync(); + } + + [Fact] + public async Task InvokeVsSolutionInSimpleLambda() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell.Interop; @@ -821,7 +828,7 @@ static void VerifyOnUIThread() { } } "; - var fix = @" + var fix = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell.Interop; @@ -841,19 +848,19 @@ static void VerifyOnUIThread() { } } "; - await new Verify.Test - { - TestCode = test, - ExpectedDiagnostics = { Verify.Diagnostic(DescriptorSync).WithSpan(12, 17, 12, 28).WithArguments("IVsSolution", "Test.VerifyOnUIThread") }, - FixedCode = fix, - CodeActionIndex = CodeFixIndex.VerifyOnUIThread, - }.RunAsync(); - } - - [Fact] - public async Task InvokeVsSolutionInLambdaWithThreadValidation() + await new CSVerify.Test { - var test = @" + TestCode = test, + ExpectedDiagnostics = { CSVerify.Diagnostic(DescriptorSync).WithSpan(12, 17, 12, 28).WithArguments("IVsSolution", "Test.VerifyOnUIThread") }, + FixedCode = fix, + CodeActionIndex = CodeFixIndex.VerifyOnUIThread, + }.RunAsync(); + } + + [Fact] + public async Task InvokeVsSolutionInLambdaWithThreadValidation() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell.Interop; @@ -871,13 +878,13 @@ void VerifyOnUIThread() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task InvokeVsSolutionInAnonymous() - { - var test = @" + [Fact] + public async Task InvokeVsSolutionInAnonymous() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell.Interop; @@ -895,7 +902,7 @@ static void VerifyOnUIThread() { } } "; - var fix = @" + var fix = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell.Interop; @@ -914,19 +921,19 @@ static void VerifyOnUIThread() { } } "; - await new Verify.Test - { - TestCode = test, - ExpectedDiagnostics = { Verify.Diagnostic(DescriptorSync).WithSpan(11, 17, 11, 28).WithArguments("IVsSolution", "Test.VerifyOnUIThread") }, - FixedCode = fix, - CodeActionIndex = CodeFixIndex.VerifyOnUIThread, - }.RunAsync(); - } - - [Fact] - public async Task GetPropertyFromVsReference() + await new CSVerify.Test { - var test = @" + TestCode = test, + ExpectedDiagnostics = { CSVerify.Diagnostic(DescriptorSync).WithSpan(11, 17, 11, 28).WithArguments("IVsSolution", "Test.VerifyOnUIThread") }, + FixedCode = fix, + CodeActionIndex = CodeFixIndex.VerifyOnUIThread, + }.RunAsync(); + } + + [Fact] + public async Task GetPropertyFromVsReference() + { + var test = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -937,14 +944,14 @@ void F() { } } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithSpan(8, 22, 8, 26).WithArguments("IVsReference", "Test.VerifyOnUIThread"); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithSpan(8, 22, 8, 26).WithArguments("IVsReference", "Test.VerifyOnUIThread"); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task CastToVsSolution() - { - var test = @" + [Fact] + public async Task CastToVsSolution() + { + var test = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -955,17 +962,17 @@ void F() { } } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithLocation(8, 19).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithLocation(8, 19).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - /// - /// Verifies that the () cast operator does not produce a diagnostic when the type is to a managed type. - /// - [Fact] - public async Task CastToManagedType_ProducesNoDiagnostic() - { - var test = @" + /// + /// Verifies that the () cast operator does not produce a diagnostic when the type is to a managed type. + /// + [Fact] + public async Task CastToManagedType_ProducesNoDiagnostic() + { + var test = @" using System; namespace TestNS { @@ -981,13 +988,13 @@ void F() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task CastToVsSolutionAfterVerifyOnUIThread() - { - var test = @" + [Fact] + public async Task CastToVsSolutionAfterVerifyOnUIThread() + { + var test = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -1002,13 +1009,13 @@ void VerifyOnUIThread() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task CastToVsSolutionViaAs() - { - var test = @" + [Fact] + public async Task CastToVsSolutionViaAs() + { + var test = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -1019,14 +1026,14 @@ void F() { } } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithSpan(8, 24, 8, 38).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithSpan(8, 24, 8, 38).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task CastToVsSolutionViaIsWithPatternMatching() - { - var test = @" + [Fact] + public async Task CastToVsSolutionViaIsWithPatternMatching() + { + var test = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -1038,17 +1045,17 @@ void F() { } } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithSpan(8, 18, 8, 32).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithSpan(8, 18, 8, 32).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - /// - /// Verifies that the as cast operator does not produce a diagnostic when the type is to a managed type. - /// - [Fact] - public async Task CastToManagedTypeViaAs_ProducesNoDiagnostic() - { - var test = @" + /// + /// Verifies that the as cast operator does not produce a diagnostic when the type is to a managed type. + /// + [Fact] + public async Task CastToManagedTypeViaAs_ProducesNoDiagnostic() + { + var test = @" using System; namespace TestNS { @@ -1064,13 +1071,13 @@ void F() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task TestVsSolutionViaIs() - { - var test = @" + [Fact] + public async Task TestVsSolutionViaIs() + { + var test = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -1081,17 +1088,17 @@ void F() { } } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithSpan(8, 27, 8, 41).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithSpan(8, 27, 8, 41).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - /// - /// Verifies that the is type check operator does not produce a diagnostic when the type is to a managed type. - /// - [Fact] - public async Task CastToManagedTypeViaIs_ProducesNoDiagnostic() - { - var test = @" + /// + /// Verifies that the is type check operator does not produce a diagnostic when the type is to a managed type. + /// + [Fact] + public async Task CastToManagedTypeViaIs_ProducesNoDiagnostic() + { + var test = @" using System; namespace TestNS { @@ -1107,13 +1114,13 @@ void F() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task CastToVsSolutionViaAsAfterVerifyOnUIThread() - { - var test = @" + [Fact] + public async Task CastToVsSolutionViaAsAfterVerifyOnUIThread() + { + var test = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -1128,13 +1135,13 @@ void VerifyOnUIThread() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task InvokeVsSolutionNoCheck_InProperty_AfterThrowIfNotOnUIThread() - { - var test = @" + [Fact] + public async Task InvokeVsSolutionNoCheck_InProperty_AfterThrowIfNotOnUIThread() + { + var test = @" using System; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; @@ -1150,13 +1157,13 @@ int F { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ShouldNotThrowNullReferenceExceptionWhenCastToStringArray() - { - var test = @" + [Fact] + public async Task ShouldNotThrowNullReferenceExceptionWhenCastToStringArray() + { + var test = @" using System; class Test { @@ -1167,13 +1174,13 @@ void F() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ShouldNotReportWarningOnCastToEnum() - { - var test = @" + [Fact] + public async Task ShouldNotReportWarningOnCastToEnum() + { + var test = @" using System; using Microsoft.VisualStudio.Shell.Interop; @@ -1184,13 +1191,13 @@ void F() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task OleServiceProviderCast_OffUIThread_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task OleServiceProviderCast_OffUIThread_ProducesDiagnostic() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; @@ -1202,18 +1209,18 @@ object Foo() } } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithSpan(9, 31, 9, 85).WithArguments("IServiceProvider", "Test.VerifyOnUIThread"); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithSpan(9, 31, 9, 85).WithArguments("IServiceProvider", "Test.VerifyOnUIThread"); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - /// - /// Verifies that calling a public method of a public class is still considered requiring - /// the UI thread because it implements the IServiceProvider interface. - /// - [Fact] - public async Task GlobalServiceProvider_GetService_OffUIThread_ProducesDiagnostic() - { - var test = @" + /// + /// Verifies that calling a public method of a public class is still considered requiring + /// the UI thread because it implements the IServiceProvider interface. + /// + [Fact] + public async Task GlobalServiceProvider_GetService_OffUIThread_ProducesDiagnostic() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; @@ -1226,18 +1233,18 @@ void Foo() } "; - DiagnosticResult[] expected = - { - Verify.Diagnostic(DescriptorSync).WithSpan(9, 40, 9, 54).WithArguments("ServiceProvider", "Test.VerifyOnUIThread"), - Verify.Diagnostic(DescriptorSync).WithSpan(9, 55, 9, 65).WithArguments("ServiceProvider", "Test.VerifyOnUIThread"), - }; - await Verify.VerifyAnalyzerAsync(test, expected); - } - - [Fact] - public async Task Package_GetService_OffUIThread_ProducesDiagnostic() + DiagnosticResult[] expected = { - var test = @" + CSVerify.Diagnostic(DescriptorSync).WithSpan(9, 40, 9, 54).WithArguments("ServiceProvider", "Test.VerifyOnUIThread"), + CSVerify.Diagnostic(DescriptorSync).WithSpan(9, 55, 9, 65).WithArguments("ServiceProvider", "Test.VerifyOnUIThread"), + }; + await CSVerify.VerifyAnalyzerAsync(test, expected); + } + + [Fact] + public async Task Package_GetService_OffUIThread_ProducesDiagnostic() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; @@ -1249,14 +1256,14 @@ void Foo() { } } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithSpan(8, 29, 8, 39).WithArguments("Package", "Test.VerifyOnUIThread"); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithSpan(8, 29, 8, 39).WithArguments("Package", "Test.VerifyOnUIThread"); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task Package_GetServiceAsync_OffUIThread_ProducesNoDiagnostic() - { - var test = @" + [Fact] + public async Task Package_GetServiceAsync_OffUIThread_ProducesNoDiagnostic() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; @@ -1276,13 +1283,13 @@ async Task Foo() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task Package_GetServiceAsync_ThenCast_OffUIThread_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task Package_GetServiceAsync_ThenCast_OffUIThread_ProducesDiagnostic() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; @@ -1301,7 +1308,7 @@ async Task Foo() { } } "; - var fix = @" + var fix = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; @@ -1322,23 +1329,23 @@ async Task Foo() { } "; - await new Verify.Test + await new CSVerify.Test + { + TestCode = test, + ExpectedDiagnostics = { - TestCode = test, - ExpectedDiagnostics = - { - Verify.Diagnostic(DescriptorAsync).WithSpan(15, 61, 15, 72).WithArguments("IVsShell", "JoinableTaskFactory.SwitchToMainThreadAsync"), - Verify.Diagnostic(DescriptorAsync).WithSpan(16, 56, 16, 67).WithArguments("IVsShell", "JoinableTaskFactory.SwitchToMainThreadAsync"), - }, - FixedCode = fix, - CodeActionIndex = CodeFixIndex.NotThreadHelper, - }.RunAsync(); - } + CSVerify.Diagnostic(DescriptorAsync).WithSpan(15, 61, 15, 72).WithArguments("IVsShell", "JoinableTaskFactory.SwitchToMainThreadAsync"), + CSVerify.Diagnostic(DescriptorAsync).WithSpan(16, 56, 16, 67).WithArguments("IVsShell", "JoinableTaskFactory.SwitchToMainThreadAsync"), + }, + FixedCode = fix, + CodeActionIndex = CodeFixIndex.NotThreadHelper, + }.RunAsync(); + } - [Fact] - public async Task SwitchMethodFoundFromOtherStaticType() - { - var test = @" + [Fact] + public async Task SwitchMethodFoundFromOtherStaticType() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; @@ -1353,7 +1360,7 @@ static async Task Foo() { } } "; - var fix = @" + var fix = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; @@ -1369,19 +1376,19 @@ static async Task Foo() { } } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorAsync).WithSpan(12, 65, 12, 76).WithArguments("IVsShell", "JoinableTaskFactory.SwitchToMainThreadAsync"); - await new Verify.Test - { - TestCode = test, - ExpectedDiagnostics = { expected }, - FixedCode = fix, - }.RunAsync(); - } - - [Fact] - public async Task TaskReturningNonAsyncMethod() + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorAsync).WithSpan(12, 65, 12, 76).WithArguments("IVsShell", "JoinableTaskFactory.SwitchToMainThreadAsync"); + await new CSVerify.Test { - var test = @" + TestCode = test, + ExpectedDiagnostics = { expected }, + FixedCode = fix, + }.RunAsync(); + } + + [Fact] + public async Task TaskReturningNonAsyncMethod() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; @@ -1399,7 +1406,7 @@ static Task Foo() { } "; #pragma warning disable CS0219 // Variable is assigned but its value is never used - var fix = @" + var fix = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; @@ -1418,15 +1425,15 @@ static async Task Foo() { " #pragma warning restore CS0219 // Variable is assigned but its value is never used ; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithSpan(13, 59, 13, 70).WithArguments("IVsShell", "Test.VerifyOnUIThread"); - await Verify.VerifyCodeFixAsync(test, expected, test); // till we have it implemented. - ////await Verify.VerifyCodeFixAsync(test, expected, fix); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithSpan(13, 59, 13, 70).WithArguments("IVsShell", "Test.VerifyOnUIThread"); + await CSVerify.VerifyCodeFixAsync(test, expected, test); // till we have it implemented. + ////await Verify.VerifyCodeFixAsync(test, expected, fix); + } - [Fact] - public async Task CodeFixAddsSwitchCallWithCancellationToken() - { - var test = @" + [Fact] + public async Task CodeFixAddsSwitchCallWithCancellationToken() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; @@ -1442,7 +1449,7 @@ protected override async Task InitializeAsync(System.Threading.CancellationToken } } "; - var fix = @" + var fix = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; @@ -1459,19 +1466,19 @@ protected override async Task InitializeAsync(System.Threading.CancellationToken } } "; - await new Verify.Test - { - TestCode = test, - ExpectedDiagnostics = { Verify.Diagnostic(DescriptorAsync).WithSpan(13, 65, 13, 76).WithArguments("IVsShell", "JoinableTaskFactory.SwitchToMainThreadAsync") }, - FixedCode = fix, - CodeActionIndex = CodeFixIndex.NotThreadHelper, - }.RunAsync(); - } - - [Fact] - public async Task CodeFixAddsSwitchCallWithCancellationTokenAsNamedParameter() + await new CSVerify.Test { - var test = @" + TestCode = test, + ExpectedDiagnostics = { CSVerify.Diagnostic(DescriptorAsync).WithSpan(13, 65, 13, 76).WithArguments("IVsShell", "JoinableTaskFactory.SwitchToMainThreadAsync") }, + FixedCode = fix, + CodeActionIndex = CodeFixIndex.NotThreadHelper, + }.RunAsync(); + } + + [Fact] + public async Task CodeFixAddsSwitchCallWithCancellationTokenAsNamedParameter() + { + var test = @" using System; using System.Threading; using System.Threading.Tasks; @@ -1491,7 +1498,7 @@ protected override async Task InitializeAsync(System.Threading.CancellationToken } } "; - var fix = @" + var fix = @" using System; using System.Threading; using System.Threading.Tasks; @@ -1512,19 +1519,19 @@ protected override async Task InitializeAsync(System.Threading.CancellationToken } } "; - await new Verify.Test - { - TestCode = test, - ExpectedDiagnostics = { Verify.Diagnostic(DescriptorAsync).WithSpan(17, 65, 17, 76).WithArguments("IVsShell", "JoinableTaskFactory.SwitchToMainThreadAsync") }, - FixedCode = fix, - CodeActionIndex = CodeFixIndex.MySwitchingMethodAsync, - }.RunAsync(); - } - - [Fact] - public async Task InterfaceAccessByClassMethod_OffUIThread_ProducesDiagnostic() + await new CSVerify.Test { - var test = @" + TestCode = test, + ExpectedDiagnostics = { CSVerify.Diagnostic(DescriptorAsync).WithSpan(17, 65, 17, 76).WithArguments("IVsShell", "JoinableTaskFactory.SwitchToMainThreadAsync") }, + FixedCode = fix, + CodeActionIndex = CodeFixIndex.MySwitchingMethodAsync, + }.RunAsync(); + } + + [Fact] + public async Task InterfaceAccessByClassMethod_OffUIThread_ProducesDiagnostic() + { + var test = @" using System; using Microsoft.VisualStudio.Shell; @@ -1542,14 +1549,14 @@ void Foo() { } } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithSpan(15, 14, 15, 26).WithArguments("IServiceProvider", "Test.VerifyOnUIThread"); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithSpan(15, 14, 15, 26).WithArguments("IServiceProvider", "Test.VerifyOnUIThread"); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task MainThreadRequiringTypes_SupportsExclusionFromWildcard() - { - var test = @" + [Fact] + public async Task MainThreadRequiringTypes_SupportsExclusionFromWildcard() + { + var test = @" using System; using Microsoft.VisualStudio.Shell; @@ -1575,18 +1582,18 @@ interface SingleThreadedType { void Foo(); } interface FreeThreadedType { void Foo(); } } "; - DiagnosticResult[] expected = - { - Verify.Diagnostic(DescriptorSync).WithSpan(9, 41, 9, 44).WithArguments("SingleThreadedType", "Test.VerifyOnUIThread"), - Verify.Diagnostic(DescriptorSync).WithSpan(11, 42, 11, 45).WithArguments("SingleThreadedType", "Test.VerifyOnUIThread"), - }; - await Verify.VerifyAnalyzerAsync(test, expected); - } - - [Fact] - public async Task NegatedMethodOverridesMatchingWildcardType() + DiagnosticResult[] expected = { - var test = @" + CSVerify.Diagnostic(DescriptorSync).WithSpan(9, 41, 9, 44).WithArguments("SingleThreadedType", "Test.VerifyOnUIThread"), + CSVerify.Diagnostic(DescriptorSync).WithSpan(11, 42, 11, 45).WithArguments("SingleThreadedType", "Test.VerifyOnUIThread"), + }; + await CSVerify.VerifyAnalyzerAsync(test, expected); + } + + [Fact] + public async Task NegatedMethodOverridesMatchingWildcardType() + { + var test = @" namespace TestNS2 { class A // this type inherits thread affinity from a wildcard match on TestNS2.* @@ -1603,14 +1610,14 @@ void Foo() } } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithSpan(13, 18, 13, 41).WithArguments("A", "Test.VerifyOnUIThread"); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithSpan(13, 18, 13, 41).WithArguments("A", "Test.VerifyOnUIThread"); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task Properties() - { - var test = @" + [Fact] + public async Task Properties() + { + var test = @" class A { string UIPropertyName { get; set; } @@ -1622,18 +1629,18 @@ void Foo() } } "; - DiagnosticResult[] expected = - { - Verify.Diagnostic(DescriptorSync).WithSpan(8, 25, 8, 39).WithArguments("A", "Test.VerifyOnUIThread"), - Verify.Diagnostic(DescriptorSync).WithSpan(9, 14, 9, 28).WithArguments("A", "Test.VerifyOnUIThread"), - }; - await Verify.VerifyAnalyzerAsync(test, expected); - } - - [Fact] - public async Task Events() + DiagnosticResult[] expected = { - var test = @" + CSVerify.Diagnostic(DescriptorSync).WithSpan(8, 25, 8, 39).WithArguments("A", "Test.VerifyOnUIThread"), + CSVerify.Diagnostic(DescriptorSync).WithSpan(9, 14, 9, 28).WithArguments("A", "Test.VerifyOnUIThread"), + }; + await CSVerify.VerifyAnalyzerAsync(test, expected); + } + + [Fact] + public async Task Events() + { + var test = @" namespace TestNS { interface SomeInterface @@ -1655,22 +1662,22 @@ void Test(TestNS.SomeInterface i) } } "; - DiagnosticResult[] expected = - { - Verify.Diagnostic(DescriptorSync).WithSpan(18, 11, 18, 22).WithArguments("SomeInterface", "Test.VerifyOnUIThread"), - Verify.Diagnostic(DescriptorSync).WithSpan(19, 11, 19, 22).WithArguments("SomeInterface", "Test.VerifyOnUIThread"), - }; - await Verify.VerifyAnalyzerAsync(test, expected); - } - - /// - /// Field initializers should never have thread affinity since the thread cannot be enforced before the code is executed, - /// since initializers run before the user-defined constructor. - /// - [Fact] - public async Task FieldInitializers() + DiagnosticResult[] expected = { - var test = @" + CSVerify.Diagnostic(DescriptorSync).WithSpan(18, 11, 18, 22).WithArguments("SomeInterface", "Test.VerifyOnUIThread"), + CSVerify.Diagnostic(DescriptorSync).WithSpan(19, 11, 19, 22).WithArguments("SomeInterface", "Test.VerifyOnUIThread"), + }; + await CSVerify.VerifyAnalyzerAsync(test, expected); + } + + /// + /// Field initializers should never have thread affinity since the thread cannot be enforced before the code is executed, + /// since initializers run before the user-defined constructor. + /// + [Fact] + public async Task FieldInitializers() + { + var test = @" using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; @@ -1679,14 +1686,14 @@ class A IVsSolution solution = Package.GetGlobalService(typeof(SVsSolution)) as IVsSolution; } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithSpan(7, 74, 7, 88).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); - await Verify.VerifyCodeFixAsync(test, expected, test); // the fix (if ever implemented) will be to move the initializer to a ctor, after a thread check. - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithSpan(7, 74, 7, 88).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); + await CSVerify.VerifyCodeFixAsync(test, expected, test); // the fix (if ever implemented) will be to move the initializer to a ctor, after a thread check. + } - [Fact] - public async Task StaticFieldInitializers() - { - var test = @" + [Fact] + public async Task StaticFieldInitializers() + { + var test = @" using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; @@ -1695,14 +1702,14 @@ class A static IVsSolution solution = Package.GetGlobalService(typeof(SVsSolution)) as IVsSolution; } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithSpan(7, 81, 7, 95).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); - await Verify.VerifyCodeFixAsync(test, expected, test); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithSpan(7, 81, 7, 95).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); + await CSVerify.VerifyCodeFixAsync(test, expected, test); + } - [Fact] - public async Task FieldAnonymousFunction() - { - var test = @" + [Fact] + public async Task FieldAnonymousFunction() + { + var test = @" using System; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; @@ -1712,7 +1719,7 @@ class A Func solutionFunc = () => Package.GetGlobalService(typeof(SVsSolution)) as IVsSolution; } "; - var fix = @" + var fix = @" using System; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; @@ -1726,19 +1733,19 @@ class A }; } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithSpan(8, 90, 8, 104).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); - await new Verify.Test - { - TestCode = test, - ExpectedDiagnostics = { expected }, - FixedCode = fix, - }.RunAsync(); - } - - [Fact] - public async Task OperatorOverload() + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithSpan(8, 90, 8, 104).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); + await new CSVerify.Test { - var test = @" + TestCode = test, + ExpectedDiagnostics = { expected }, + FixedCode = fix, + }.RunAsync(); + } + + [Fact] + public async Task OperatorOverload() + { + var test = @" using System; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; @@ -1754,7 +1761,7 @@ class A public static bool operator !=(A item1, A item2) => !(item1 == item2); } "; - var fix = @" + var fix = @" using System; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; @@ -1771,19 +1778,19 @@ class A public static bool operator !=(A item1, A item2) => !(item1 == item2); } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithSpan(10, 63, 10, 77).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); - await new Verify.Test - { - TestCode = test, - ExpectedDiagnostics = { expected }, - FixedCode = fix, - }.RunAsync(); - } - - [Fact] - public async Task ArgumentExpressionEntirelyMadeOfViolatingCast() + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithSpan(10, 63, 10, 77).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); + await new CSVerify.Test { - var test = @" + TestCode = test, + ExpectedDiagnostics = { expected }, + FixedCode = fix, + }.RunAsync(); + } + + [Fact] + public async Task ArgumentExpressionEntirelyMadeOfViolatingCast() + { + var test = @" using Microsoft.VisualStudio.Shell.Interop; class A @@ -1796,7 +1803,7 @@ void Foo() { void Bar(IVsSolution solution) { } } "; - var fix = @" + var fix = @" using Microsoft.VisualStudio.Shell.Interop; class A @@ -1810,19 +1817,19 @@ void Foo() { void Bar(IVsSolution solution) { } } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorSync).WithSpan(8, 13, 8, 27).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); - await new Verify.Test - { - TestCode = test, - ExpectedDiagnostics = { expected }, - FixedCode = fix, - }.RunAsync(); - } - - [Fact] - public async Task AffinityPropagationExtendsToAllCallersOfSyncMethods() + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorSync).WithSpan(8, 13, 8, 27).WithArguments("IVsSolution", "Test.VerifyOnUIThread"); + await new CSVerify.Test { - var test = @" + TestCode = test, + ExpectedDiagnostics = { expected }, + FixedCode = fix, + }.RunAsync(); + } + + [Fact] + public async Task AffinityPropagationExtendsToAllCallersOfSyncMethods() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; @@ -1850,19 +1857,19 @@ async void SecondAsync() } } "; - var expected = new DiagnosticResult[] - { - Verify.Diagnostic(DescriptorSync).WithSpan(11, 9, 11, 12).WithArguments("Test.Foo", "Test.VerifyOnUIThread"), - Verify.Diagnostic(DescriptorAsync).WithSpan(21, 9, 21, 14).WithArguments("Test.Reset", "JoinableTaskFactory.SwitchToMainThreadAsync"), - }; + var expected = new DiagnosticResult[] + { + CSVerify.Diagnostic(DescriptorSync).WithSpan(11, 9, 11, 12).WithArguments("Test.Foo", "Test.VerifyOnUIThread"), + CSVerify.Diagnostic(DescriptorAsync).WithSpan(21, 9, 21, 14).WithArguments("Test.Reset", "JoinableTaskFactory.SwitchToMainThreadAsync"), + }; - await Verify.VerifyAnalyzerAsync(test, expected); - } + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task AffinityPropagationDoesNotExtendBeyondProperAsyncSwitch() - { - var test = @" + [Fact] + public async Task AffinityPropagationDoesNotExtendBeyondProperAsyncSwitch() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; @@ -1890,17 +1897,17 @@ async void SecondAsync() } } "; - var expect = new DiagnosticResult[] - { - Verify.Diagnostic(DescriptorSync).WithSpan(11, 9, 11, 12).WithArguments("Test.Foo", "Test.VerifyOnUIThread"), - }; - await Verify.VerifyAnalyzerAsync(test, expect); - } - - [Fact] - public async Task StructMembers() + var expect = new DiagnosticResult[] { - var test = @" + CSVerify.Diagnostic(DescriptorSync).WithSpan(11, 9, 11, 12).WithArguments("Test.Foo", "Test.VerifyOnUIThread"), + }; + await CSVerify.VerifyAnalyzerAsync(test, expect); + } + + [Fact] + public async Task StructMembers() + { + var test = @" namespace TestNS { struct SomeStruct @@ -1928,23 +1935,22 @@ static void Main() } } "; - var expect = new DiagnosticResult[] - { - Verify.Diagnostic(DescriptorSync).WithSpan(20, 31, 20, 42).WithArguments("SomeStruct", "Test.VerifyOnUIThread"), - Verify.Diagnostic(DescriptorSync).WithSpan(23, 16, 23, 20).WithArguments("SomeStruct", "Test.VerifyOnUIThread"), - Verify.Diagnostic(DescriptorSync).WithSpan(24, 29, 24, 33).WithArguments("SomeStruct", "Test.VerifyOnUIThread"), - }; - await Verify.VerifyAnalyzerAsync(test, expect); - } - - private static class CodeFixIndex + var expect = new DiagnosticResult[] { - public const int SwitchToMainThreadAsync = 0; - public const int ThrowIfNotOnUIThreadIndex0 = 0; - public const int ThrowIfNotOnUIThreadIndex1 = 1; - public const int VerifyOnUIThread = 0; - public const int NotThreadHelper = 0; - public const int MySwitchingMethodAsync = 0; - } + CSVerify.Diagnostic(DescriptorSync).WithSpan(20, 31, 20, 42).WithArguments("SomeStruct", "Test.VerifyOnUIThread"), + CSVerify.Diagnostic(DescriptorSync).WithSpan(23, 16, 23, 20).WithArguments("SomeStruct", "Test.VerifyOnUIThread"), + CSVerify.Diagnostic(DescriptorSync).WithSpan(24, 29, 24, 33).WithArguments("SomeStruct", "Test.VerifyOnUIThread"), + }; + await CSVerify.VerifyAnalyzerAsync(test, expect); + } + + private static class CodeFixIndex + { + public const int SwitchToMainThreadAsync = 0; + public const int ThrowIfNotOnUIThreadIndex0 = 0; + public const int ThrowIfNotOnUIThreadIndex1 = 1; + public const int VerifyOnUIThread = 0; + public const int NotThreadHelper = 0; + public const int MySwitchingMethodAsync = 0; } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD011UseAsyncLazyAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD011UseAsyncLazyAnalyzerTests.cs index 29924acc2..c58a2b281 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD011UseAsyncLazyAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD011UseAsyncLazyAnalyzerTests.cs @@ -1,20 +1,14 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Testing; - using Xunit; - using Verify = CSharpCodeFixVerifier; +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; - public class VSTHRD011UseAsyncLazyAnalyzerTests +public class VSTHRD011UseAsyncLazyAnalyzerTests +{ + [Fact] + public async Task ReportErrorOnLazyOfTConstructionInFieldValueTypeArg() { - [Fact] - public async Task ReportErrorOnLazyOfTConstructionInFieldValueTypeArg() - { - var test = @" + var test = @" using System; using System.Threading.Tasks; @@ -24,14 +18,14 @@ class Test { Lazy tInt = new Lazy(); } "; - DiagnosticResult expected = this.CreateDiagnostic(AbstractVSTHRD011UseAsyncLazyAnalyzer.LazyOfTaskDescriptor, 0); - await Verify.VerifyAnalyzerAsync(test, expected); - } - - [Fact] - public async Task ReportErrorOnLazyOfTConstructionInFieldRefTypeArg() - { - var test = @" + DiagnosticResult expected = this.CreateDiagnostic(AbstractVSTHRD011UseAsyncLazyAnalyzer.LazyOfTaskDescriptor, 0); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } + + [Fact] + public async Task ReportErrorOnLazyOfTConstructionInFieldRefTypeArg() + { + var test = @" using System; using System.Threading.Tasks; @@ -39,14 +33,14 @@ class Test { Lazy> t3 = new {|#0:Lazy>|}(); } "; - DiagnosticResult expected = this.CreateDiagnostic(AbstractVSTHRD011UseAsyncLazyAnalyzer.LazyOfTaskDescriptor, 0); - await Verify.VerifyAnalyzerAsync(test, expected); - } - - [Fact] - public async Task ReportErrorOnLazyOfTConstructionInFieldNoTypeArg() - { - var test = @" + DiagnosticResult expected = this.CreateDiagnostic(AbstractVSTHRD011UseAsyncLazyAnalyzer.LazyOfTaskDescriptor, 0); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } + + [Fact] + public async Task ReportErrorOnLazyOfTConstructionInFieldNoTypeArg() + { + var test = @" using System; using System.Threading.Tasks; @@ -54,14 +48,14 @@ class Test { Lazy t3 = new {|#0:Lazy|}(); } "; - DiagnosticResult expected = this.CreateDiagnostic(AbstractVSTHRD011UseAsyncLazyAnalyzer.LazyOfTaskDescriptor, 0); - await Verify.VerifyAnalyzerAsync(test, expected); - } - - [Fact] - public async Task JTFRunInLazyValueFactory_Delegate() - { - var test = @" + DiagnosticResult expected = this.CreateDiagnostic(AbstractVSTHRD011UseAsyncLazyAnalyzer.LazyOfTaskDescriptor, 0); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } + + [Fact] + public async Task JTFRunInLazyValueFactory_Delegate() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -80,14 +74,14 @@ void Foo() { } } "; - DiagnosticResult expected = this.CreateDiagnostic(AbstractVSTHRD011UseAsyncLazyAnalyzer.SyncBlockInValueFactoryDescriptor, 0); - await Verify.VerifyAnalyzerAsync(test, expected); - } - - [Fact] - public async Task JTFRunInLazyValueFactory_Lambda() - { - var test = @" + DiagnosticResult expected = this.CreateDiagnostic(AbstractVSTHRD011UseAsyncLazyAnalyzer.SyncBlockInValueFactoryDescriptor, 0); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } + + [Fact] + public async Task JTFRunInLazyValueFactory_Lambda() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -106,14 +100,14 @@ void Foo() { } } "; - DiagnosticResult expected = this.CreateDiagnostic(AbstractVSTHRD011UseAsyncLazyAnalyzer.SyncBlockInValueFactoryDescriptor, 0); - await Verify.VerifyAnalyzerAsync(test, expected); - } - - [Fact] - public async Task JTFRunAsyncInLazyValueFactory_Lambda() - { - var test = @" + DiagnosticResult expected = this.CreateDiagnostic(AbstractVSTHRD011UseAsyncLazyAnalyzer.SyncBlockInValueFactoryDescriptor, 0); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } + + [Fact] + public async Task JTFRunAsyncInLazyValueFactory_Lambda() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -132,14 +126,14 @@ await jtf.RunAsync(async delegate { } } "; - DiagnosticResult expected = this.CreateDiagnostic(AbstractVSTHRD011UseAsyncLazyAnalyzer.LazyOfTaskDescriptor, 0); - await Verify.VerifyAnalyzerAsync(test, expected); - } - - [Fact] - public async Task JTFRunInLazyValueFactory_MethodGroup() - { - var test = @" + DiagnosticResult expected = this.CreateDiagnostic(AbstractVSTHRD011UseAsyncLazyAnalyzer.LazyOfTaskDescriptor, 0); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } + + [Fact] + public async Task JTFRunInLazyValueFactory_MethodGroup() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -161,14 +155,14 @@ int LazyValueFactory() { } "; - // We can change this to verify a diagnostic is reported if we ever implement this. - await Verify.VerifyAnalyzerAsync(test); - } + // We can change this to verify a diagnostic is reported if we ever implement this. + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ReportErrorOnLazyOfTConstructionInLocalVariable() - { - var test = @" + [Fact] + public async Task ReportErrorOnLazyOfTConstructionInLocalVariable() + { + var test = @" using System; using System.Threading.Tasks; @@ -178,11 +172,10 @@ void Foo() { } } "; - DiagnosticResult expected = this.CreateDiagnostic(AbstractVSTHRD011UseAsyncLazyAnalyzer.LazyOfTaskDescriptor, 0); - await Verify.VerifyAnalyzerAsync(test, expected); - } - - private DiagnosticResult CreateDiagnostic(DiagnosticDescriptor descriptor, int location) - => Verify.Diagnostic(descriptor).WithLocation(location); + DiagnosticResult expected = this.CreateDiagnostic(AbstractVSTHRD011UseAsyncLazyAnalyzer.LazyOfTaskDescriptor, 0); + await CSVerify.VerifyAnalyzerAsync(test, expected); } + + private DiagnosticResult CreateDiagnostic(DiagnosticDescriptor descriptor, int location) + => CSVerify.Diagnostic(descriptor).WithLocation(location); } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD012SpecifyJtfWhereAllowedTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD012SpecifyJtfWhereAllowedTests.cs index 0a6e9ed3a..a9cfa155a 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD012SpecifyJtfWhereAllowedTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD012SpecifyJtfWhereAllowedTests.cs @@ -1,19 +1,14 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System.Threading.Tasks; - using Microsoft.CodeAnalysis.Testing; - using Xunit; - using Verify = CSharpCodeFixVerifier; +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; - public class VSTHRD012SpecifyJtfWhereAllowedTests +public class VSTHRD012SpecifyJtfWhereAllowedTests +{ + [Fact] + public async Task SiblingMethodOverloads_WithoutJTF_GeneratesWarning() { - [Fact] - public async Task SiblingMethodOverloads_WithoutJTF_GeneratesWarning() - { - var test = @" + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -27,19 +22,19 @@ void G(JoinableTaskFactory jtf) { } } "; - DiagnosticResult expected = Verify.Diagnostic().WithLocation(0); - await new Verify.Test - { - TestCode = test, - ExpectedDiagnostics = { expected }, - TestBehaviors = TestBehaviors.SkipGeneratedCodeCheck, - }.RunAsync(); - } - - [Fact] - public async Task SiblingMethodOverloads_WithoutJTC_GeneratesWarning() + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(0); + await new CSVerify.Test { - var test = @" + TestCode = test, + ExpectedDiagnostics = { expected }, + TestBehaviors = TestBehaviors.SkipGeneratedCodeCheck, + }.RunAsync(); + } + + [Fact] + public async Task SiblingMethodOverloads_WithoutJTC_GeneratesWarning() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -53,19 +48,19 @@ void G(JoinableTaskContext jtc) { } } "; - DiagnosticResult expected = Verify.Diagnostic().WithLocation(0); - await new Verify.Test - { - TestCode = test, - ExpectedDiagnostics = { expected }, - TestBehaviors = TestBehaviors.SkipGeneratedCodeCheck, - }.RunAsync(); - } - - [Fact] - public async Task SiblingMethodOverloadsWithOptionalAttribute_WithoutJTC_GeneratesNoWarning() + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(0); + await new CSVerify.Test { - var test = @" + TestCode = test, + ExpectedDiagnostics = { expected }, + TestBehaviors = TestBehaviors.SkipGeneratedCodeCheck, + }.RunAsync(); + } + + [Fact] + public async Task SiblingMethodOverloadsWithOptionalAttribute_WithoutJTC_GeneratesNoWarning() + { + var test = @" using System.Threading.Tasks; using System.Runtime.InteropServices; using Microsoft.VisualStudio.Threading; @@ -80,13 +75,13 @@ void G([Optional] JoinableTaskContext jtc) { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task SiblingMethodOverloadsWithObsoleteAttribute_WithoutJTC_GeneratesNoWarning() - { - var test = @" + [Fact] + public async Task SiblingMethodOverloadsWithObsoleteAttribute_WithoutJTC_GeneratesNoWarning() + { + var test = @" using System; using Microsoft.VisualStudio.Threading; @@ -101,13 +96,13 @@ void G(JoinableTaskContext jtc) { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task SiblingCtorOverloads_WithoutJTF_GeneratesWarning() - { - var test = @" + [Fact] + public async Task SiblingCtorOverloads_WithoutJTF_GeneratesWarning() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -123,19 +118,19 @@ internal Apple(JoinableTaskFactory jtf) { } } "; - DiagnosticResult expected = Verify.Diagnostic().WithLocation(0); - await new Verify.Test - { - TestCode = test, - ExpectedDiagnostics = { expected }, - TestBehaviors = TestBehaviors.SkipGeneratedCodeCheck, - }.RunAsync(); - } - - [Fact] - public async Task SiblingMethodOverloads_WithJTF_GeneratesNoWarning() + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(0); + await new CSVerify.Test { - var test = @" + TestCode = test, + ExpectedDiagnostics = { expected }, + TestBehaviors = TestBehaviors.SkipGeneratedCodeCheck, + }.RunAsync(); + } + + [Fact] + public async Task SiblingMethodOverloads_WithJTF_GeneratesNoWarning() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -152,13 +147,13 @@ void G(JoinableTaskContext jtc) { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task AsyncLazy_WithoutJTF_GeneratesWarning() - { - var test = @" + [Fact] + public async Task AsyncLazy_WithoutJTF_GeneratesWarning() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -169,19 +164,19 @@ void F() { } "; - DiagnosticResult expected = Verify.Diagnostic().WithLocation(0); - await new Verify.Test - { - TestCode = test, - ExpectedDiagnostics = { expected }, - TestBehaviors = TestBehaviors.SkipGeneratedCodeCheck, - }.RunAsync(); - } - - [Fact] - public async Task AsyncLazy_WithNullJTF_GeneratesNoWarning() + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(0); + await new CSVerify.Test { - var test = @" + TestCode = test, + ExpectedDiagnostics = { expected }, + TestBehaviors = TestBehaviors.SkipGeneratedCodeCheck, + }.RunAsync(); + } + + [Fact] + public async Task AsyncLazy_WithNullJTF_GeneratesNoWarning() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -192,13 +187,13 @@ void F() { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task AsyncLazy_WithJTF_GeneratesNoWarning() - { - var test = @" + [Fact] + public async Task AsyncLazy_WithJTF_GeneratesNoWarning() + { + var test = @" using Microsoft.VisualStudio.Threading; using System.Threading.Tasks; @@ -211,13 +206,13 @@ void F() { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task JTF_RunAsync_GeneratesNoWarning() - { - var test = @" + [Fact] + public async Task JTF_RunAsync_GeneratesNoWarning() + { + var test = @" using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; using System.Threading.Tasks; @@ -236,13 +231,13 @@ async delegate { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task JTF_Ctor_GeneratesNoWarning() - { - var test = @" + [Fact] + public async Task JTF_Ctor_GeneratesNoWarning() + { + var test = @" using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; using System.Threading.Tasks; @@ -257,7 +252,63 @@ void F() { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task InaccessibleMembers_Private_GeneratesNoWarning() + { + string test = """ + using System; + using Microsoft.VisualStudio.Threading; + + static class Extensions + { + public static void OnMainThread(Action action) => OnMainThread(null, action); + + private static void OnMainThread(JoinableTaskFactory factory, Action action) => factory.Run(async delegate + { + await factory.SwitchToMainThreadAsync(); + action(); + }); + } + + class Foo + { + void Bar() + { + Extensions.OnMainThread(() => { }); + } + } + """; + + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task AccessibleMembers_Private_GeneratesWarning() + { + string test = """ + using System; + using Microsoft.VisualStudio.Threading; + + static class Extensions + { + public static void OnMainThread(Action action) => OnMainThread(null, action); + + private static void OnMainThread(JoinableTaskFactory factory, Action action) => factory.Run(async delegate + { + await factory.SwitchToMainThreadAsync(); + action(); + }); + + static void Bar() + { + [|OnMainThread|](() => { }); + } + } + """; + + await CSVerify.VerifyAnalyzerAsync(test); } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD100AsyncVoidMethodAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD100AsyncVoidMethodAnalyzerTests.cs index 71eb23477..d84e0e61a 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD100AsyncVoidMethodAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD100AsyncVoidMethodAnalyzerTests.cs @@ -1,18 +1,14 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System.Threading.Tasks; - using Xunit; - using Verify = CSharpCodeFixVerifier; +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; - public class VSTHRD100AsyncVoidMethodAnalyzerTests +public class VSTHRD100AsyncVoidMethodAnalyzerTests +{ + [Fact] + public async Task ReportWarningOnAsyncVoidMethod() { - [Fact] - public async Task ReportWarningOnAsyncVoidMethod() - { - var test = @" + var test = @" using System; class Test { @@ -20,14 +16,32 @@ async void F() { } } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithLocation(5, 16); - await Verify.VerifyAnalyzerAsync(test, expected); - } - - [Fact] - public async Task ReportWarningOnAsyncVoidMethodSimilarToAsyncEventHandler() - { - var test = @" + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(5, 16); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } + + [Fact] + public async Task ReportWarningOnAsyncVoidLocalFunction() + { + var test = @" +using System; + +class Test { + void M() { + F(); + + async void F() {} + } +} +"; + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(8, 20); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } + + [Fact] + public async Task ReportWarningOnAsyncVoidMethodSimilarToAsyncEventHandler() + { + var test = @" using System; class Test { @@ -35,14 +49,14 @@ async void F(object sender, object e) { } } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithLocation(5, 16); - await Verify.VerifyAnalyzerAsync(test, expected); - } - - [Fact] - public async Task ReportWarningOnAsyncVoidEventHandlerSimilarToAsyncEventHandler2() - { - var test = @" + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(5, 16); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } + + [Fact] + public async Task ReportWarningOnAsyncVoidEventHandlerSimilarToAsyncEventHandler2() + { + var test = @" using System; class Test { @@ -50,14 +64,14 @@ async void F(string sender, EventArgs e) { } } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithLocation(5, 16); - await Verify.VerifyAnalyzerAsync(test, expected); - } - - [Fact] - public async Task ReportWarningOnAsyncVoidEventHandler() - { - var test = @" + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(5, 16); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } + + [Fact] + public async Task ReportWarningOnAsyncVoidEventHandler() + { + var test = @" using System; class Test { @@ -65,7 +79,7 @@ async void F(object sender, EventArgs e) { } } "; - var withFix = @" + var withFix = @" using System; class Test { @@ -73,14 +87,14 @@ async System.Threading.Tasks.Task F(object sender, EventArgs e) { } } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithLocation(5, 16); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } - - [Fact] - public async Task ReportWarningOnAsyncVoidEventHandlerWithMyEventArgs() - { - var test = @" + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(5, 16); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } + + [Fact] + public async Task ReportWarningOnAsyncVoidEventHandlerWithMyEventArgs() + { + var test = @" using System; class Test { @@ -90,7 +104,7 @@ async void F(object sender, MyEventArgs e) { class MyEventArgs : EventArgs {} "; - var withFix = @" + var withFix = @" using System; class Test { @@ -100,8 +114,7 @@ async System.Threading.Tasks.Task F(object sender, MyEventArgs e) { class MyEventArgs : EventArgs {} "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithLocation(5, 16); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(5, 16); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD100AsyncVoidMethodCodeFixTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD100AsyncVoidMethodCodeFixTests.cs index 7e3ea7526..0501ce8dc 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD100AsyncVoidMethodCodeFixTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD100AsyncVoidMethodCodeFixTests.cs @@ -1,18 +1,14 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System.Threading.Tasks; - using Xunit; - using Verify = CSharpCodeFixVerifier; +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; - public class VSTHRD100AsyncVoidMethodCodeFixTests +public class VSTHRD100AsyncVoidMethodCodeFixTests +{ + [Fact] + public async Task ApplyFixesOnAsyncVoidMethod() { - [Fact] - public async Task ApplyFixesOnAsyncVoidMethod() - { - var test = @" + var test = @" using System; class Test { @@ -21,7 +17,7 @@ async void F() { } } "; - var withFix = @" + var withFix = @" using System; class Test { @@ -30,14 +26,14 @@ async System.Threading.Tasks.Task F() { } } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithLocation(5, 16); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(5, 16); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task ApplyFixesOnAsyncVoidMethod2() - { - var test = @" + [Fact] + public async Task ApplyFixesOnAsyncVoidMethod2() + { + var test = @" using System; using System.Threading.Tasks; @@ -47,7 +43,7 @@ async void F() { } } "; - var withFix = @" + var withFix = @" using System; using System.Threading.Tasks; @@ -57,8 +53,7 @@ async Task F() { } } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithLocation(6, 16); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(6, 16); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD101AsyncVoidLambdaAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD101AsyncVoidLambdaAnalyzerTests.cs index f125f63b8..9ab1b841a 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD101AsyncVoidLambdaAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD101AsyncVoidLambdaAnalyzerTests.cs @@ -1,19 +1,14 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System.Threading.Tasks; - using Microsoft.CodeAnalysis.Testing; - using Xunit; - using Verify = CSharpCodeFixVerifier; +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; - public class VSTHRD101AsyncVoidLambdaAnalyzerTests +public class VSTHRD101AsyncVoidLambdaAnalyzerTests +{ + [Fact] + public async Task ReportWarningOnAsyncVoidLambda() { - [Fact] - public async Task ReportWarningOnAsyncVoidLambda() - { - var test = @" + var test = @" using System; class Test { @@ -26,14 +21,14 @@ void T() { } } "; - DiagnosticResult expected = Verify.Diagnostic().WithLocation(0); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(0); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task ReportWarningOnAsyncVoidLambdaWithOneParameter() - { - var test = @" + [Fact] + public async Task ReportWarningOnAsyncVoidLambdaWithOneParameter() + { + var test = @" using System; class Test { @@ -46,14 +41,14 @@ void T() { } } "; - DiagnosticResult expected = Verify.Diagnostic().WithLocation(0); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(0); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task ReportWarningOnAsyncVoidLambdaWithOneParameter2() - { - var test = @" + [Fact] + public async Task ReportWarningOnAsyncVoidLambdaWithOneParameter2() + { + var test = @" using System; class Test { @@ -66,14 +61,14 @@ void T() { } } "; - DiagnosticResult expected = Verify.Diagnostic().WithLocation(0); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(0); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task ReportWarningOnAsyncVoidAnonymousDelegateWithOneParameter() - { - var test = @" + [Fact] + public async Task ReportWarningOnAsyncVoidAnonymousDelegateWithOneParameter() + { + var test = @" using System; class Test { @@ -86,14 +81,14 @@ void T() { } } "; - DiagnosticResult expected = Verify.Diagnostic().WithLocation(0); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(0); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task ReportWarningOnAsyncVoidLambdaSetToVariable() - { - var test = @" + [Fact] + public async Task ReportWarningOnAsyncVoidLambdaSetToVariable() + { + var test = @" using System; class Test { @@ -102,14 +97,14 @@ void F() { } } "; - DiagnosticResult expected = Verify.Diagnostic().WithLocation(0); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(0); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task ReportWarningOnAsyncVoidLambdaWithOneParameterSetToVariable() - { - var test = @" + [Fact] + public async Task ReportWarningOnAsyncVoidLambdaWithOneParameterSetToVariable() + { + var test = @" using System; class Test { @@ -118,14 +113,14 @@ void F() { } } "; - DiagnosticResult expected = Verify.Diagnostic().WithLocation(0); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(0); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task ReportWarningOnAsyncVoidLambdaBeingUsedAsEventHandler() - { - var test = @" + [Fact] + public async Task ReportWarningOnAsyncVoidLambdaBeingUsedAsEventHandler() + { + var test = @" using System; class Test { @@ -137,12 +132,11 @@ void F() { class MyEventArgs : EventArgs {} } "; - DiagnosticResult[] expected = - { - Verify.Diagnostic().WithLocation(0), - Verify.Diagnostic().WithLocation(1), - }; - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult[] expected = + { + CSVerify.Diagnostic().WithLocation(0), + CSVerify.Diagnostic().WithLocation(1), + }; + await CSVerify.VerifyAnalyzerAsync(test, expected); } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD102AvoidJtfRunInNonPublicMembersAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD102AvoidJtfRunInNonPublicMembersAnalyzerTests.cs index a690f1e03..cbe31a6c0 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD102AvoidJtfRunInNonPublicMembersAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD102AvoidJtfRunInNonPublicMembersAnalyzerTests.cs @@ -1,19 +1,14 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Xunit; - using Verify = CSharpCodeFixVerifier; +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; - public class VSTHRD102AvoidJtfRunInNonPublicMembersAnalyzerTests +public class VSTHRD102AvoidJtfRunInNonPublicMembersAnalyzerTests +{ + [Fact] + public async Task JtfRunInPublicMethodsOfInternalType_ProducesDiagnostic() { - [Fact] - public async Task JtfRunInPublicMethodsOfInternalType_ProducesDiagnostic() - { - var test = @" + var test = @" using Microsoft.VisualStudio.Threading; class Test { @@ -24,14 +19,14 @@ public void F() { } } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithLocation(8, 13); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(8, 13); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task JtfRunInPublicMethodsOfPublicType_DoesNotProduceDiagnostic() - { - var test = @" + [Fact] + public async Task JtfRunInPublicMethodsOfPublicType_DoesNotProduceDiagnostic() + { + var test = @" using Microsoft.VisualStudio.Threading; public class Test { @@ -42,13 +37,13 @@ public void F() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task JtfRunInTaskReturningMethod_DoesNotProduceDiagnostic() - { - var test = @" + [Fact] + public async Task JtfRunInTaskReturningMethod_DoesNotProduceDiagnostic() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -61,13 +56,13 @@ public Task F() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task JtfRunInProtectedMethodsOfInternalType_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task JtfRunInProtectedMethodsOfInternalType_ProducesDiagnostic() + { + var test = @" using Microsoft.VisualStudio.Threading; class Test { @@ -78,14 +73,14 @@ protected void F() { } } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithLocation(8, 13); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(8, 13); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task JtfRunInProtectedMethodsOfPublicType_DoesNotProduceDiagnostic() - { - var test = @" + [Fact] + public async Task JtfRunInProtectedMethodsOfPublicType_DoesNotProduceDiagnostic() + { + var test = @" using Microsoft.VisualStudio.Threading; public class Test { @@ -96,13 +91,13 @@ protected void F() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task JtfRunInExplicitlyInternalMethods_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task JtfRunInExplicitlyInternalMethods_ProducesDiagnostic() + { + var test = @" using Microsoft.VisualStudio.Threading; class Test { @@ -113,14 +108,14 @@ internal void F() { } } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithLocation(8, 13); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(8, 13); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task JtfRunInImplicitlyInternalMethods_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task JtfRunInImplicitlyInternalMethods_ProducesDiagnostic() + { + var test = @" using Microsoft.VisualStudio.Threading; class Test { @@ -131,14 +126,14 @@ void F() { } } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithLocation(8, 13); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(8, 13); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task JtfRunAllowedInMainMethod_DoesNotProduceDiagnostic() - { - var test = @" + [Fact] + public async Task JtfRunAllowedInMainMethod_DoesNotProduceDiagnostic() + { + var test = @" using Microsoft.VisualStudio.Threading; class Program { @@ -149,20 +144,20 @@ static void Main() { } } "; - await new Verify.Test - { - TestState = - { - Sources = { test }, - OutputKind = OutputKind.ConsoleApplication, - }, - }.RunAsync(); - } - - [Fact] - public async Task JtfRunInExplicitInterfaceImplementationOfInternalInterface_ProducesDiagnostic() + await new CSVerify.Test { - var test = @" + TestState = + { + Sources = { test }, + OutputKind = OutputKind.ConsoleApplication, + }, + }.RunAsync(); + } + + [Fact] + public async Task JtfRunInExplicitInterfaceImplementationOfInternalInterface_ProducesDiagnostic() + { + var test = @" using Microsoft.VisualStudio.Threading; interface IFoo @@ -178,14 +173,14 @@ void IFoo.F() { } } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithLocation(13, 13); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(13, 13); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task JtfRunInImplicitInterfaceImplementationOfInternalInterface_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task JtfRunInImplicitInterfaceImplementationOfInternalInterface_ProducesDiagnostic() + { + var test = @" using Microsoft.VisualStudio.Threading; interface IFoo @@ -201,14 +196,14 @@ public void F() { } } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithLocation(13, 13); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(13, 13); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task JtfRunInExplicitInterfaceImplementationOfPublicInterface_ProducesNoDiagnostic() - { - var test = @" + [Fact] + public async Task JtfRunInExplicitInterfaceImplementationOfPublicInterface_ProducesNoDiagnostic() + { + var test = @" using Microsoft.VisualStudio.Threading; public interface IFoo @@ -224,13 +219,13 @@ void IFoo.F() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task JtfRunInImplicitInterfaceImplementationOfPublicInterface_ProducesNoDiagnostic() - { - var test = @" + [Fact] + public async Task JtfRunInImplicitInterfaceImplementationOfPublicInterface_ProducesNoDiagnostic() + { + var test = @" using Microsoft.VisualStudio.Threading; public interface IFoo @@ -246,13 +241,13 @@ public void F() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task JtfRunInPublicConstructorOfInternalType_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task JtfRunInPublicConstructorOfInternalType_ProducesDiagnostic() + { + var test = @" using Microsoft.VisualStudio.Threading; class Test { @@ -263,14 +258,14 @@ public Test() { } } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithLocation(8, 13); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(8, 13); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task JtfRunInPublicConstructorOfPublicType_DoesNotProduceDiagnostic() - { - var test = @" + [Fact] + public async Task JtfRunInPublicConstructorOfPublicType_DoesNotProduceDiagnostic() + { + var test = @" using Microsoft.VisualStudio.Threading; public class Test { @@ -281,13 +276,13 @@ public Test() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task JtfRunAndPropertyGetterInLambda_ProducesNoDiagnostic() - { - var test = @" + [Fact] + public async Task JtfRunAndPropertyGetterInLambda_ProducesNoDiagnostic() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -304,13 +299,13 @@ void F() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task JtfRunAndPropertyGetterInAnonymousDelegate_ProducesNoDiagnostic() - { - var test = @" + [Fact] + public async Task JtfRunAndPropertyGetterInAnonymousDelegate_ProducesNoDiagnostic() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -327,13 +322,13 @@ void F() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact(Skip = "Unattainable given Roslyn analyzers are sync and find all references is async")] - public async Task JtfRunAndPropertyGetterPrivateMethodUsedAsDelegate_ProducesNoDiagnostic() - { - var test = @" + [Fact(Skip = "Unattainable given Roslyn analyzers are sync and find all references is async")] + public async Task JtfRunAndPropertyGetterPrivateMethodUsedAsDelegate_ProducesNoDiagnostic() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -354,13 +349,13 @@ void SomeSyncMethod(int x) { public void Advise(Action foo) { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task JtfRunInPrivateMethod__WithMultiMemberAccessExpression_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task JtfRunInPrivateMethod__WithMultiMemberAccessExpression_ProducesDiagnostic() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -384,7 +379,6 @@ class Foo { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD103UseAsyncOptionAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD103UseAsyncOptionAnalyzerTests.cs index 827cf5c73..0e27c2c55 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD103UseAsyncOptionAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD103UseAsyncOptionAnalyzerTests.cs @@ -1,20 +1,15 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System.Threading.Tasks; - using Microsoft.CodeAnalysis.Testing; - using Xunit; - using static Microsoft.VisualStudio.Threading.Analyzers.VSTHRD103UseAsyncOptionAnalyzer; - using Verify = CSharpCodeFixVerifier; +using static Microsoft.VisualStudio.Threading.Analyzers.VSTHRD103UseAsyncOptionAnalyzer; +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; - public class VSTHRD103UseAsyncOptionAnalyzerTests +public class VSTHRD103UseAsyncOptionAnalyzerTests +{ + [Fact] + public async Task JTFRunInTaskReturningMethodGeneratesWarning() { - [Fact] - public async Task JTFRunInTaskReturningMethodGeneratesWarning() - { - var test = @" + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -30,7 +25,7 @@ void Run() { } } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -44,14 +39,14 @@ async Task T() { void Run() { } } "; - DiagnosticResult expected = Verify.Diagnostic(Descriptor).WithLocation(8, 13).WithArguments("Run", "RunAsync"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(Descriptor).WithLocation(8, 13).WithArguments("Run", "RunAsync"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task JTFRunInTaskReturningMethod_WithExtraReturn_GeneratesWarning() - { - var test = @" + [Fact] + public async Task JTFRunInTaskReturningMethod_WithExtraReturn_GeneratesWarning() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -71,7 +66,7 @@ void Run() { } } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -89,14 +84,14 @@ async Task T() { void Run() { } } "; - DiagnosticResult expected = Verify.Diagnostic(Descriptor).WithLocation(8, 13).WithArguments("Run", "RunAsync"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(Descriptor).WithLocation(8, 13).WithArguments("Run", "RunAsync"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task JTFRunInAsyncMethodGeneratesWarning() - { - var test = @" + [Fact] + public async Task JTFRunInAsyncMethodGeneratesWarning() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -111,7 +106,7 @@ void Run() { } } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -125,14 +120,14 @@ async Task T() { void Run() { } } "; - DiagnosticResult expected = Verify.Diagnostic(Descriptor).WithLocation(8, 13).WithArguments("Run", "RunAsync"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(Descriptor).WithLocation(8, 13).WithArguments("Run", "RunAsync"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task JTFRunOfTInTaskReturningMethodGeneratesWarning() - { - var test = @" + [Fact] + public async Task JTFRunOfTInTaskReturningMethodGeneratesWarning() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -148,7 +143,7 @@ void Run() { } } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -163,14 +158,14 @@ void Run() { } } "; - DiagnosticResult expected = Verify.Diagnostic(Descriptor).WithLocation(8, 26).WithArguments("Run", "RunAsync"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(Descriptor).WithLocation(8, 26).WithArguments("Run", "RunAsync"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task JTJoinOfTInTaskReturningMethodGeneratesWarning() - { - var test = @" + [Fact] + public async Task JTJoinOfTInTaskReturningMethodGeneratesWarning() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -187,7 +182,7 @@ void Join() { } } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -203,14 +198,14 @@ void Join() { } } "; - DiagnosticResult expected = Verify.Diagnostic(Descriptor).WithLocation(9, 12).WithArguments("Join", "JoinAsync"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(Descriptor).WithLocation(9, 12).WithArguments("Join", "JoinAsync"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task TaskWaitInTaskReturningMethodGeneratesWarning() - { - var test = @" + [Fact] + public async Task TaskWaitInTaskReturningMethodGeneratesWarning() + { + var test = @" using System.Threading.Tasks; class Test { @@ -222,7 +217,7 @@ Task T() { } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; class Test { @@ -232,14 +227,14 @@ async Task T() { } } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorNoAlternativeMethod).WithLocation(7, 11).WithArguments("Wait"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorNoAlternativeMethod).WithLocation(7, 11).WithArguments("Wait"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task TaskWaitInValueTaskReturningMethodGeneratesWarning() - { - var test = @" + [Fact] + public async Task TaskWaitInValueTaskReturningMethodGeneratesWarning() + { + var test = @" using System.Threading.Tasks; class Test { @@ -251,7 +246,7 @@ ValueTask T() { } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; class Test { @@ -261,13 +256,13 @@ async ValueTask T() { } } "; - await Verify.VerifyCodeFixAsync(test, Verify.Diagnostic(DescriptorNoAlternativeMethod).WithLocation(0).WithArguments("Wait"), withFix); - } + await CSVerify.VerifyCodeFixAsync(test, CSVerify.Diagnostic(DescriptorNoAlternativeMethod).WithLocation(0).WithArguments("Wait"), withFix); + } - [Fact] - public async Task TaskWait_InIAsyncEnumerableAsyncMethod_ShouldReportWarning() - { - var test = @" + [Fact] + public async Task TaskWait_InIAsyncEnumerableAsyncMethod_ShouldReportWarning() + { + var test = @" using System; using System.Collections.Generic; using System.Linq; @@ -281,7 +276,7 @@ async IAsyncEnumerable FooAsync() } } "; - var withFix = @" + var withFix = @" using System; using System.Collections.Generic; using System.Linq; @@ -295,13 +290,13 @@ async IAsyncEnumerable FooAsync() } } "; - await Verify.VerifyCodeFixAsync(test, Verify.Diagnostic(DescriptorNoAlternativeMethod).WithLocation(0).WithArguments("Wait"), withFix); - } + await CSVerify.VerifyCodeFixAsync(test, CSVerify.Diagnostic(DescriptorNoAlternativeMethod).WithLocation(0).WithArguments("Wait"), withFix); + } - [Fact] - public async Task IVsTaskWaitInTaskReturningMethodGeneratesWarning() - { - var test = @" + [Fact] + public async Task IVsTaskWaitInTaskReturningMethodGeneratesWarning() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; @@ -316,7 +311,7 @@ Task T() { } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; @@ -329,14 +324,14 @@ async Task T() { } } "; - DiagnosticResult expected = this.CreateDiagnostic(10, 11, 4, "Wait"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = this.CreateDiagnostic(10, 11, 4, "Wait"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task IVsTaskGetResultInTaskReturningMethodGeneratesWarning() - { - var test = @" + [Fact] + public async Task IVsTaskGetResultInTaskReturningMethodGeneratesWarning() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; @@ -351,7 +346,7 @@ Task T() { } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; @@ -364,17 +359,17 @@ async Task T() { } } "; - DiagnosticResult expected = this.CreateDiagnostic(10, 27, 9, "GetResult"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = this.CreateDiagnostic(10, 27, 9, "GetResult"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - /// - /// Ensures we don't offer a code fix when the required using directive is not already present. - /// - [Fact] - public async Task IVsTaskGetResultInTaskReturningMethod_WithoutUsing_OffersNoFix() - { - var test = @" + /// + /// Ensures we don't offer a code fix when the required using directive is not already present. + /// + [Fact] + public async Task IVsTaskGetResultInTaskReturningMethod_WithoutUsing_OffersNoFix() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell.Interop; @@ -387,40 +382,40 @@ Task T() { } "; - string withFix = test; - //// var withFix = @" - //// using System.Threading.Tasks; - //// using Microsoft.VisualStudio.Shell; - //// using Microsoft.VisualStudio.Shell.Interop; - //// using Task = System.Threading.Tasks.Task; - //// - //// class Test { - //// async Task T() { - //// IVsTask t = null; - //// object result = await t; - //// } - //// } - //// "; - DiagnosticResult expected = this.CreateDiagnostic(8, 27, 9, "GetResult"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + string withFix = test; + //// var withFix = @" + //// using System.Threading.Tasks; + //// using Microsoft.VisualStudio.Shell; + //// using Microsoft.VisualStudio.Shell.Interop; + //// using Task = System.Threading.Tasks.Task; + //// + //// class Test { + //// async Task T() { + //// IVsTask t = null; + //// object result = await t; + //// } + //// } + //// "; + DiagnosticResult expected = this.CreateDiagnostic(8, 27, 9, "GetResult"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task TaskOfTResultInTaskReturningMethodGeneratesWarning() - { - var test = @" + [Fact] + public async Task TaskOfTResultInTaskReturningMethodGeneratesWarning() + { + var test = @" using System.Threading.Tasks; class Test { Task T() { Task t = null; - int result = t.Result; + int result = t.{|#0:Result|}; return Task.FromResult(result); } } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; class Test { @@ -432,14 +427,52 @@ async Task T() { } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorNoAlternativeMethod).WithLocation(7, 24).WithArguments("Result"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorNoAlternativeMethod).WithLocation(0).WithArguments("Result"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } + + [Fact] + public async Task TaskOfTResultInTaskReturningMethodGeneratesWarning_ConditionalAccess() + { + var test = @" +using System.Threading.Tasks; + +class Test { + Task T() { + Task t = null; + int? result = t?.{|#0:Result|}; + return Task.FromResult(result); + } +} +"; + + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorNoAlternativeMethod).WithLocation(0).WithArguments("Result"); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } + + [Fact] + public async Task TaskOfTResultInTaskReturningMethodGeneratesWarning_ConditionalAccess2() + { + var test = @" +using System.Threading.Tasks; + +class Test { + Task T() { + Task t = null; + int result = t?.{|#0:Result|} ?? 1; + return Task.FromResult(result); + } +} +"; + + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorNoAlternativeMethod).WithLocation(0).WithArguments("Result"); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task TaskOfTResultInTaskReturningMethodGeneratesWarning_FixPreservesCall() - { - var test = @" + [Fact] + public async Task TaskOfTResultInTaskReturningMethodGeneratesWarning_FixPreservesCall() + { + var test = @" using System.Threading.Tasks; class Test { @@ -455,7 +488,7 @@ static class Assert { } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; class Test { @@ -470,14 +503,14 @@ static class Assert { } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorNoAlternativeMethod).WithLocation(7, 26).WithArguments("Result"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorNoAlternativeMethod).WithLocation(7, 26).WithArguments("Result"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task TaskOfTResultInTaskReturningMethodGeneratesWarning_FixRewritesCorrectExpression() - { - var test = @" + [Fact] + public async Task TaskOfTResultInTaskReturningMethodGeneratesWarning_FixRewritesCorrectExpression() + { + var test = @" using System; using System.Threading.Tasks; @@ -488,7 +521,7 @@ async Task T() { } "; - var withFix = @" + var withFix = @" using System; using System.Threading.Tasks; @@ -499,14 +532,14 @@ async Task T() { } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorNoAlternativeMethod).WithLocation(7, 45).WithArguments("Result"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorNoAlternativeMethod).WithLocation(7, 45).WithArguments("Result"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task TaskOfTResultInTaskReturningAnonymousMethodWithinSyncMethod_GeneratesWarning() - { - var test = @" + [Fact] + public async Task TaskOfTResultInTaskReturningAnonymousMethodWithinSyncMethod_GeneratesWarning() + { + var test = @" using System; using System.Threading.Tasks; @@ -521,7 +554,7 @@ void T() { } "; - var withFix = @" + var withFix = @" using System; using System.Threading.Tasks; @@ -536,14 +569,14 @@ void T() { } "; - DiagnosticResult expected = this.CreateDiagnostic(9, 28, 6, "Result"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = this.CreateDiagnostic(9, 28, 6, "Result"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task TaskOfTResultInTaskReturningSimpleLambdaWithinSyncMethod_GeneratesWarning() - { - var test = @" + [Fact] + public async Task TaskOfTResultInTaskReturningSimpleLambdaWithinSyncMethod_GeneratesWarning() + { + var test = @" using System; using System.Threading.Tasks; @@ -558,7 +591,7 @@ void T() { } "; - var withFix = @" + var withFix = @" using System; using System.Threading.Tasks; @@ -573,14 +606,14 @@ void T() { } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorNoAlternativeMethod).WithSpan(9, 28, 9, 34).WithArguments("Result"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorNoAlternativeMethod).WithSpan(9, 28, 9, 34).WithArguments("Result"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task TaskOfTResultInTaskReturningSimpleLambdaExpressionWithinSyncMethod_GeneratesWarning() - { - var test = @" + [Fact] + public async Task TaskOfTResultInTaskReturningSimpleLambdaExpressionWithinSyncMethod_GeneratesWarning() + { + var test = @" using System; using System.Threading.Tasks; @@ -592,7 +625,7 @@ void T() { } "; - var withFix = @" + var withFix = @" using System; using System.Threading.Tasks; @@ -604,14 +637,14 @@ void T() { } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorNoAlternativeMethod).WithSpan(8, 57, 8, 63).WithArguments("Result"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorNoAlternativeMethod).WithSpan(8, 57, 8, 63).WithArguments("Result"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task TaskOfTResultInTaskReturningParentheticalLambdaWithinSyncMethod_GeneratesWarning() - { - var test = @" + [Fact] + public async Task TaskOfTResultInTaskReturningParentheticalLambdaWithinSyncMethod_GeneratesWarning() + { + var test = @" using System; using System.Threading.Tasks; @@ -626,7 +659,7 @@ void T() { } "; - var withFix = @" + var withFix = @" using System; using System.Threading.Tasks; @@ -641,14 +674,14 @@ void T() { } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorNoAlternativeMethod).WithSpan(9, 28, 9, 34).WithArguments("Result"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorNoAlternativeMethod).WithSpan(9, 28, 9, 34).WithArguments("Result"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task TaskOfTResultInTaskReturningMethodAnonymousDelegate_GeneratesNoWarning() - { - var test = @" + [Fact] + public async Task TaskOfTResultInTaskReturningMethodAnonymousDelegate_GeneratesNoWarning() + { + var test = @" using System; using System.Threading.Tasks; @@ -661,13 +694,13 @@ Task T() { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task TaskGetAwaiterGetResultInTaskReturningMethodGeneratesWarning() - { - var test = @" + [Fact] + public async Task TaskGetAwaiterGetResultInTaskReturningMethodGeneratesWarning() + { + var test = @" using System.Threading.Tasks; class Test { @@ -679,7 +712,7 @@ Task T() { } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; class Test { @@ -690,14 +723,14 @@ async Task T() { } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorNoAlternativeMethod).WithLocation(7, 24).WithArguments("GetResult"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorNoAlternativeMethod).WithLocation(7, 24).WithArguments("GetResult"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task SyncInvocationWhereAsyncOptionExistsInSameTypeGeneratesWarning() - { - var test = @" + [Fact] + public async Task SyncInvocationWhereAsyncOptionExistsInSameTypeGeneratesWarning() + { + var test = @" using System.Threading.Tasks; class Test { @@ -711,7 +744,7 @@ internal static void Foo(int x, int y) { } } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; class Test { @@ -724,14 +757,14 @@ internal static void Foo(int x, int y) { } } "; - DiagnosticResult expected = Verify.Diagnostic(Descriptor).WithSpan(6, 9, 6, 12).WithArguments("Foo", "FooAsync"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(Descriptor).WithSpan(6, 9, 6, 12).WithArguments("Foo", "FooAsync"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task SyncInvocationWhereAsyncOptionIsObsolete_GeneratesNoWarning() - { - var test = @" + [Fact] + public async Task SyncInvocationWhereAsyncOptionIsObsolete_GeneratesNoWarning() + { + var test = @" using System.Threading.Tasks; class Test { @@ -746,13 +779,13 @@ internal static void Foo(int x, int y) { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task SyncInvocationWhereAsyncOptionIsPartlyObsolete_GeneratesWarning() - { - var test = @" + [Fact] + public async Task SyncInvocationWhereAsyncOptionIsPartlyObsolete_GeneratesWarning() + { + var test = @" using System.Threading.Tasks; class Test { @@ -769,7 +802,7 @@ internal static void Foo(int x, double y) { } } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; class Test { @@ -785,14 +818,14 @@ internal static void Foo(int x, double y) { } } "; - DiagnosticResult expected = Verify.Diagnostic(Descriptor).WithSpan(6, 9, 6, 12).WithArguments("Foo", "FooAsync"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(Descriptor).WithSpan(6, 9, 6, 12).WithArguments("Foo", "FooAsync"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task SyncInvocationWhereAsyncOptionExistsInSubExpressionGeneratesWarning() - { - var test = @" + [Fact] + public async Task SyncInvocationWhereAsyncOptionExistsInSubExpressionGeneratesWarning() + { + var test = @" using System.Threading.Tasks; class Test { @@ -806,7 +839,7 @@ Task T() { } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; class Test { @@ -819,14 +852,14 @@ async Task T() { } "; - DiagnosticResult expected = Verify.Diagnostic(Descriptor).WithSpan(6, 17, 6, 20).WithArguments("Foo", "FooAsync"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(Descriptor).WithSpan(6, 17, 6, 20).WithArguments("Foo", "FooAsync"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task SyncInvocationWhereAsyncOptionExistsInOtherTypeGeneratesWarning() - { - var test = @" + [Fact] + public async Task SyncInvocationWhereAsyncOptionExistsInOtherTypeGeneratesWarning() + { + var test = @" using System.Threading.Tasks; class Test { @@ -842,7 +875,7 @@ internal static void Foo() { } } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; class Test { @@ -857,14 +890,14 @@ internal static void Foo() { } } "; - DiagnosticResult expected = Verify.Diagnostic(Descriptor).WithSpan(6, 14, 6, 17).WithArguments("Foo", "FooAsync"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(Descriptor).WithSpan(6, 14, 6, 17).WithArguments("Foo", "FooAsync"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task SyncInvocationWhereAsyncOptionExistsAsPrivateInOtherTypeGeneratesNoWarning() - { - var test = @" + [Fact] + public async Task SyncInvocationWhereAsyncOptionExistsAsPrivateInOtherTypeGeneratesNoWarning() + { + var test = @" using System.Threading.Tasks; class Test { @@ -880,13 +913,13 @@ internal static void Foo() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task SyncInvocationWhereAsyncOptionExistsInOtherBaseTypeGeneratesWarning() - { - var test = @" + [Fact] + public async Task SyncInvocationWhereAsyncOptionExistsInOtherBaseTypeGeneratesWarning() + { + var test = @" using System.Threading.Tasks; class Test { @@ -906,7 +939,7 @@ internal void Foo() { } } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; class Test { @@ -925,14 +958,14 @@ internal void Foo() { } } "; - DiagnosticResult expected = Verify.Diagnostic(Descriptor).WithSpan(7, 11, 7, 14).WithArguments("Foo", "FooAsync"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(Descriptor).WithSpan(7, 11, 7, 14).WithArguments("Foo", "FooAsync"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task SyncInvocationWhereAsyncOptionExistsInExtensionMethodGeneratesWarning() - { - var test = @" + [Fact] + public async Task SyncInvocationWhereAsyncOptionExistsInExtensionMethodGeneratesWarning() + { + var test = @" using System.Threading.Tasks; class Test { @@ -952,7 +985,7 @@ static class FruitUtils { } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; class Test { @@ -971,14 +1004,14 @@ static class FruitUtils { } "; - DiagnosticResult expected = Verify.Diagnostic(Descriptor).WithSpan(7, 11, 7, 14).WithArguments("Foo", "FooAsync"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(Descriptor).WithSpan(7, 11, 7, 14).WithArguments("Foo", "FooAsync"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task SyncInvocationUsingStaticGeneratesWarning() - { - var test = @" + [Fact] + public async Task SyncInvocationUsingStaticGeneratesWarning() + { + var test = @" using System.Threading.Tasks; using static FruitUtils; @@ -995,7 +1028,7 @@ internal static void Foo() { } } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; using static FruitUtils; @@ -1011,14 +1044,14 @@ internal static void Foo() { } } "; - DiagnosticResult expected = Verify.Diagnostic(Descriptor).WithSpan(7, 9, 7, 12).WithArguments("Foo", "FooAsync"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(Descriptor).WithSpan(7, 9, 7, 12).WithArguments("Foo", "FooAsync"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task SyncInvocationUsingStaticGeneratesNoWarningAcrossTypes() - { - var test = @" + [Fact] + public async Task SyncInvocationUsingStaticGeneratesNoWarningAcrossTypes() + { + var test = @" using System.Threading.Tasks; using static FruitUtils; using static PlateUtils; @@ -1042,13 +1075,13 @@ static class PlateUtils { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task AwaitingAsyncMethodWithoutSuffixProducesNoWarningWhereSuffixVersionExists() - { - var test = @" + [Fact] + public async Task AwaitingAsyncMethodWithoutSuffixProducesNoWarningWhereSuffixVersionExists() + { + var test = @" using System.Threading.Tasks; class Test { @@ -1061,20 +1094,20 @@ async Task BarAsync() { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - /// - /// Verifies that when method invocations and member access happens in properties - /// (which can never be async), nothing bad happens. - /// - /// - /// This may like a trivially simple case. But guess why we had to add a test for it? (it failed). - /// - [Fact] - public async Task NoDiagnosticAndNoExceptionForProperties() - { - var test = @" + /// + /// Verifies that when method invocations and member access happens in properties + /// (which can never be async), nothing bad happens. + /// + /// + /// This may like a trivially simple case. But guess why we had to add a test for it? (it failed). + /// + [Fact] + public async Task NoDiagnosticAndNoExceptionForProperties() + { + var test = @" using System.Threading.Tasks; class Test { @@ -1083,13 +1116,13 @@ class Test { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task GenericMethodName() - { - var test = @" + [Fact] + public async Task GenericMethodName() + { + var test = @" using System.Threading.Tasks; using static FruitUtils; @@ -1106,7 +1139,7 @@ internal static void Foo() { } } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; using static FruitUtils; @@ -1122,14 +1155,14 @@ internal static void Foo() { } } "; - DiagnosticResult expected = Verify.Diagnostic(Descriptor).WithSpan(7, 9, 7, 17).WithArguments("Foo", "FooAsync"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(Descriptor).WithSpan(7, 9, 7, 17).WithArguments("Foo", "FooAsync"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task AsyncAlternative_CodeFixRespectsTrivia() - { - var test = @" + [Fact] + public async Task AsyncAlternative_CodeFixRespectsTrivia() + { + var test = @" using System; using System.Threading.Tasks; @@ -1147,7 +1180,7 @@ async Task DoWorkAsync() } } "; - var withFix = @" + var withFix = @" using System; using System.Threading.Tasks; @@ -1165,14 +1198,14 @@ async Task DoWorkAsync() } } "; - DiagnosticResult expected = Verify.Diagnostic(Descriptor).WithSpan(15, 9, 15, 12).WithArguments("Foo", "FooAsync"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(Descriptor).WithSpan(15, 9, 15, 12).WithArguments("Foo", "FooAsync"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task AwaitRatherThanWait_CodeFixRespectsTrivia() - { - var test = @" + [Fact] + public async Task AwaitRatherThanWait_CodeFixRespectsTrivia() + { + var test = @" using System; using System.Threading.Tasks; @@ -1190,7 +1223,7 @@ async Task DoWorkAsync() } } "; - var withFix = @" + var withFix = @" using System; using System.Threading.Tasks; @@ -1208,14 +1241,14 @@ async Task DoWorkAsync() } } "; - DiagnosticResult expected = Verify.Diagnostic(DescriptorNoAlternativeMethod).WithSpan(15, 34, 15, 38).WithArguments("Wait"); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(DescriptorNoAlternativeMethod).WithSpan(15, 34, 15, 38).WithArguments("Wait"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task XunitThrowAsyncNotSuggestedInAsyncTestMethod() - { - var test = @" + [Fact] + public async Task XunitThrowAsyncNotSuggestedInAsyncTestMethod() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -1231,13 +1264,13 @@ void Throws(Action action) { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task DoNotSuggestAsyncAlternativeWhenItIsSelf() - { - var test = @" + [Fact] + public async Task DoNotSuggestAsyncAlternativeWhenItIsSelf() + { + var test = @" using System; using System.Threading.Tasks; @@ -1256,13 +1289,13 @@ public void CallMain() } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task DoNotSuggestAsyncAlternativeWhenItReturnsVoid() - { - var test = @" + [Fact] + public async Task DoNotSuggestAsyncAlternativeWhenItReturnsVoid() + { + var test = @" using System; using System.Threading.Tasks; @@ -1278,13 +1311,157 @@ Task MethodAsync() } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task DoNotRaiseInSyncLocalFunctionInsideAsyncMethod() + { + string test = """ + using System.Threading.Tasks; - private DiagnosticResult CreateDiagnostic(int line, int column, int length, string methodName) - => Verify.Diagnostic(DescriptorNoAlternativeMethod).WithSpan(line, column, line, column + length).WithArguments(methodName); + class SomeClass { + Task Foo() + { + return Task.CompletedTask; - private DiagnosticResult CreateDiagnostic(int line, int column, int length, string methodName, string alternativeMethodName) - => Verify.Diagnostic(Descriptor).WithSpan(line, column, line, column + length).WithArguments(methodName, alternativeMethodName); + void CompletionHandler() + { + this.Bar(); + } + } + + void Bar() {} + Task BarAsync() => Task.CompletedTask; + } + """; + + await CSVerify.VerifyAnalyzerAsync(test); } + + [Fact] + public async Task SyncMethodCallInAsyncMethod_ExcludedViaAdditionalFiles_GeneratesNoWarning() + { + var test = @" +using System.Threading.Tasks; + +class Test { + async Task T() { + TestNamespace.TestClass.SlowSyncMethod(); + } +} + +namespace TestNamespace { + class TestClass { + public static void SlowSyncMethod() { } + public static Task SlowSyncMethodAsync() => Task.CompletedTask; + } +} +"; + + // No diagnostic expected because SlowSyncMethod is excluded via AdditionalFiles + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task SyncMethodCallInAsyncMethod_NotExcludedViaAdditionalFiles_GeneratesWarning() + { + var test = @" +using System.Threading.Tasks; + +class Test { + async Task T() { + TestNamespace.TestClass.{|#0:NotExcludedMethod|}(); + } +} + +namespace TestNamespace { + class TestClass { + public static void NotExcludedMethod() { } + public static Task NotExcludedMethodAsync() => Task.CompletedTask; + } +} +"; + + await CSVerify.VerifyAnalyzerAsync(test, CSVerify.Diagnostic(Descriptor).WithLocation(0).WithArguments("NotExcludedMethod", "NotExcludedMethodAsync")); + } + + [Fact] + public async Task DoNotRaiseForDistinctSyncMethod() + { + string test = @" +using System.Threading.Tasks; + +class SomeClass { + Task Method(){ + Bar(10, 11); + return Task.CompletedTask; + } + + Task Foo() => Task.FromResult(11); + async Task BarAsync(int id) { + var number = await Foo(); + return Bar(id, number); + } + int Bar(int id, int number) => id * number; +} +"; + + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task SyncExtensionMethodWhereAsyncAlternativeExistsInSameStaticClassGeneratesWarning() + { + var test = @" +using System.Threading.Tasks; + +public interface IExecutable { } + +public static class ExecutableExtensions +{ + public static string GetOutput(this IExecutable executable) => """"; + public static Task GetOutputAsync(this IExecutable executable) => Task.FromResult(""""); +} + +class Test +{ + async Task DoWorkAsync() + { + IExecutable exec = null!; + string result = exec.{|#0:GetOutput|}(); + } +} +"; + + var withFix = @" +using System.Threading.Tasks; + +public interface IExecutable { } + +public static class ExecutableExtensions +{ + public static string GetOutput(this IExecutable executable) => """"; + public static Task GetOutputAsync(this IExecutable executable) => Task.FromResult(""""); +} + +class Test +{ + async Task DoWorkAsync() + { + IExecutable exec = null!; + string result = await exec.GetOutputAsync(); + } +} +"; + + DiagnosticResult expected = CSVerify.Diagnostic(Descriptor).WithLocation(0).WithArguments("GetOutput", "GetOutputAsync"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } + + private DiagnosticResult CreateDiagnostic(int line, int column, int length, string methodName) + => CSVerify.Diagnostic(DescriptorNoAlternativeMethod).WithSpan(line, column, line, column + length).WithArguments(methodName); + + private DiagnosticResult CreateDiagnostic(int line, int column, int length, string methodName, string alternativeMethodName) + => CSVerify.Diagnostic(Descriptor).WithSpan(line, column, line, column + length).WithArguments(methodName, alternativeMethodName); } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD104OfferAsyncOptionAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD104OfferAsyncOptionAnalyzerTests.cs index ec1440d0e..8b0acc4eb 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD104OfferAsyncOptionAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD104OfferAsyncOptionAnalyzerTests.cs @@ -1,18 +1,14 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System.Threading.Tasks; - using Xunit; - using Verify = CSharpCodeFixVerifier; +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; - public class VSTHRD104OfferAsyncOptionAnalyzerTests +public class VSTHRD104OfferAsyncOptionAnalyzerTests +{ + [Fact] + public async Task JTFRunFromPublicVoidMethod_GeneratesWarning() { - [Fact] - public async Task JTFRunFromPublicVoidMethod_GeneratesWarning() - { - var test = @" + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -27,14 +23,14 @@ public void Foo() { } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithSpan(9, 13, 9, 16); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithSpan(9, 13, 9, 16); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task JTFRunFromInternalVoidMethod_GeneratesNoWarning() - { - var test = @" + [Fact] + public async Task JTFRunFromInternalVoidMethod_GeneratesNoWarning() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -49,13 +45,13 @@ internal void Foo() { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task JTFRunFromPublicVoidMethod_GeneratesNoWarningWhenAsyncMethodPresent() - { - var test = @" + [Fact] + public async Task JTFRunFromPublicVoidMethod_GeneratesNoWarningWhenAsyncMethodPresent() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -74,13 +70,13 @@ public async Task FooAsync() { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task JTFRunFromPublicVoidMethod_GeneratesWarningWhenInternalAsyncMethodPresent() - { - var test = @" + [Fact] + public async Task JTFRunFromPublicVoidMethod_GeneratesWarningWhenInternalAsyncMethodPresent() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -99,8 +95,7 @@ internal async Task FooAsync() { } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithSpan(9, 13, 9, 16); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithSpan(9, 13, 9, 16); + await CSVerify.VerifyAnalyzerAsync(test, expected); } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzerTests.cs index fc9e2fa62..ffeae6a29 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzerTests.cs @@ -1,19 +1,14 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System.Threading.Tasks; - using Microsoft.CodeAnalysis.Testing; - using Xunit; - using Verify = CSharpCodeFixVerifier; +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; - public class VSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzerTests +public class VSTHRD105AvoidImplicitTaskSchedulerCurrentAnalyzerTests +{ + [Fact] + public async Task ContinueWith_NoTaskScheduler_GeneratesWarning() { - [Fact] - public async Task ContinueWith_NoTaskScheduler_GeneratesWarning() - { - var test = @" + var test = @" using System.Threading.Tasks; class Test { @@ -24,19 +19,19 @@ void F() { } "; - DiagnosticResult expected = Verify.Diagnostic().WithLocation(0); - await new Verify.Test - { - TestCode = test, - ExpectedDiagnostics = { expected }, - TestBehaviors = TestBehaviors.SkipGeneratedCodeCheck, - }.RunAsync(); - } - - [Fact] - public async Task StartNew_NoTaskScheduler_GeneratesWarning() + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(0); + await new CSVerify.Test { - var test = @" + TestCode = test, + ExpectedDiagnostics = { expected }, + TestBehaviors = TestBehaviors.SkipGeneratedCodeCheck, + }.RunAsync(); + } + + [Fact] + public async Task StartNew_NoTaskScheduler_GeneratesWarning() + { + var test = @" using System.Threading.Tasks; class Test { @@ -46,19 +41,19 @@ void F() { } "; - DiagnosticResult expected = Verify.Diagnostic().WithLocation(0); - await new Verify.Test - { - TestCode = test, - ExpectedDiagnostics = { expected }, - TestBehaviors = TestBehaviors.SkipGeneratedCodeCheck, - }.RunAsync(); - } - - [Fact] - public async Task StartNew_NoTaskScheduler_GeneratesNoWarningOnCustomTaskFactory() + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(0); + await new CSVerify.Test { - var test = @" + TestCode = test, + ExpectedDiagnostics = { expected }, + TestBehaviors = TestBehaviors.SkipGeneratedCodeCheck, + }.RunAsync(); + } + + [Fact] + public async Task StartNew_NoTaskScheduler_GeneratesNoWarningOnCustomTaskFactory() + { + var test = @" using System.Threading.Tasks; class Test { @@ -70,13 +65,13 @@ void F() { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ContinueWith_WithTaskScheduler_GeneratesNoWarning() - { - var test = @" + [Fact] + public async Task ContinueWith_WithTaskScheduler_GeneratesNoWarning() + { + var test = @" using System.Threading.Tasks; class Test { @@ -88,13 +83,13 @@ void F() { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task StartNew_WithTaskScheduler_GeneratesNoWarning() - { - var test = @" + [Fact] + public async Task StartNew_WithTaskScheduler_GeneratesNoWarning() + { + var test = @" using System.Threading; using System.Threading.Tasks; @@ -106,7 +101,6 @@ void F() { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD106UseInvokeAsyncForAsyncEventsAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD106UseInvokeAsyncForAsyncEventsAnalyzerTests.cs index 60cf580d9..a22af86b8 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD106UseInvokeAsyncForAsyncEventsAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD106UseInvokeAsyncForAsyncEventsAnalyzerTests.cs @@ -1,18 +1,14 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System.Threading.Tasks; - using Xunit; - using Verify = CSharpCodeFixVerifier; +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; - public class VSTHRD106UseInvokeAsyncForAsyncEventsAnalyzerTests +public class VSTHRD106UseInvokeAsyncForAsyncEventsAnalyzerTests +{ + [Fact] + public async Task ReportWarningIfInvokeAsyncEventHandlerDirectly() { - [Fact] - public async Task ReportWarningIfInvokeAsyncEventHandlerDirectly() - { - var test = @" + var test = @" using System; using System.Linq; using Microsoft.VisualStudio.Threading; @@ -30,13 +26,13 @@ void F() { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task DoNotReportWarningIfAsyncEventHandlerIsInvokedByInvokeAsync() - { - var test = @" + [Fact] + public async Task DoNotReportWarningIfAsyncEventHandlerIsInvokedByInvokeAsync() + { + var test = @" using System; using System.Linq; using Microsoft.VisualStudio.Threading; @@ -49,13 +45,13 @@ static void InvokeAsync(AsyncEventHandler handler2, AsyncEventHandler } }} "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ReportWarningIfInvokeAsyncEventHandlerDirectlyViaInvoke() - { - var test = @" + [Fact] + public async Task ReportWarningIfInvokeAsyncEventHandlerDirectlyViaInvoke() + { + var test = @" using System; using System.Linq; using Microsoft.VisualStudio.Threading; @@ -73,13 +69,13 @@ void F() { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ReportWarningIfInvokeAsyncEventHandlerDirectlyAsDelegateViaInvoke() - { - var test = @" + [Fact] + public async Task ReportWarningIfInvokeAsyncEventHandlerDirectlyAsDelegateViaInvoke() + { + var test = @" using System; using System.Linq; using Microsoft.VisualStudio.Threading; @@ -97,13 +93,13 @@ void F() { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ReportWarningIfInvokeAsyncEventHandlerDirectlyAndTheyAreLocalVariables() - { - var test = @" + [Fact] + public async Task ReportWarningIfInvokeAsyncEventHandlerDirectlyAndTheyAreLocalVariables() + { + var test = @" using System; using System.Linq; using Microsoft.VisualStudio.Threading; @@ -121,13 +117,13 @@ void F() { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ReportWarningIfInvokeAsyncEventHandlerDirectlyAndTheyAreProperties() - { - var test = @" + [Fact] + public async Task ReportWarningIfInvokeAsyncEventHandlerDirectlyAndTheyAreProperties() + { + var test = @" using System; using System.Linq; using Microsoft.VisualStudio.Threading; @@ -145,13 +141,13 @@ void F() { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ReportWarningIfInvokeAsyncEventHandlerViaInvocationList() - { - var test = @" + [Fact] + public async Task ReportWarningIfInvokeAsyncEventHandlerViaInvocationList() + { + var test = @" using System; using System.Linq; using Microsoft.VisualStudio.Threading; @@ -169,13 +165,13 @@ void F() { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ReportWarningIfInvokeAsyncEventHandlerAsDelegate() - { - var test = @" + [Fact] + public async Task ReportWarningIfInvokeAsyncEventHandlerAsDelegate() + { + var test = @" using System; using System.Linq; using Microsoft.VisualStudio.Threading; @@ -193,13 +189,13 @@ void F() { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ReportWarningIfInvokeLazyAsyncEventHandlerAsDelegate() - { - var test = @" + [Fact] + public async Task ReportWarningIfInvokeLazyAsyncEventHandlerAsDelegate() + { + var test = @" using System; using System.Linq; using Microsoft.VisualStudio.Threading; @@ -217,13 +213,13 @@ void F() { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ReportWarningIfInvokeAsyncEventHandlerDirectlyAndTheyArePassedAsParameters() - { - var test = @" + [Fact] + public async Task ReportWarningIfInvokeAsyncEventHandlerDirectlyAndTheyArePassedAsParameters() + { + var test = @" using System; using System.Linq; using Microsoft.VisualStudio.Threading; @@ -237,13 +233,13 @@ void F(AsyncEventHandler handler1, AsyncEventHandler handler2, Asy } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ReportWarningIfInvokeAsyncEventHandlerDirectlyAndTheyArePassedAsArray() - { - var test = @" + [Fact] + public async Task ReportWarningIfInvokeAsyncEventHandlerDirectlyAndTheyArePassedAsArray() + { + var test = @" using System; using System.Linq; using Microsoft.VisualStudio.Threading; @@ -257,13 +253,13 @@ void F(AsyncEventHandler[] handlers1, AsyncEventHandler[] handlers } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task DoNotReportWarningIfInvokeAsyncEventHandlerUsingInvokeAsync() - { - var test = @" + [Fact] + public async Task DoNotReportWarningIfInvokeAsyncEventHandlerUsingInvokeAsync() + { + var test = @" using System; using System.Linq; using Microsoft.VisualStudio.Threading; @@ -280,13 +276,13 @@ void F() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task DoNotReportWarningIfNonInvokeMethodsAreUsed() - { - var test = @" + [Fact] + public async Task DoNotReportWarningIfNonInvokeMethodsAreUsed() + { + var test = @" using System; using System.Linq; using Microsoft.VisualStudio.Threading; @@ -303,13 +299,13 @@ void F() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task DoNotReportWarningIfInvokeAsyncEventHandlerUsingInvokeAsyncAndTheyArePassedAsParameters() - { - var test = @" + [Fact] + public async Task DoNotReportWarningIfInvokeAsyncEventHandlerUsingInvokeAsyncAndTheyArePassedAsParameters() + { + var test = @" using System; using System.Linq; using Microsoft.VisualStudio.Threading; @@ -322,13 +318,13 @@ void F(AsyncEventHandler handler1, AsyncEventHandler handler2, Asy } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task DoNotReportWarningIfInvokeAsyncEventHandlerUsingInvokeAsyncAndTheyArePassedAsArray() - { - var test = @" + [Fact] + public async Task DoNotReportWarningIfInvokeAsyncEventHandlerUsingInvokeAsyncAndTheyArePassedAsArray() + { + var test = @" using System; using System.Linq; using Microsoft.VisualStudio.Threading; @@ -341,7 +337,6 @@ void F(AsyncEventHandler[] handlers1, AsyncEventHandler[] handlers } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD107AwaitTaskWithinUsingExpressionAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD107AwaitTaskWithinUsingExpressionAnalyzerTests.cs index 7d801e820..2656c456b 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD107AwaitTaskWithinUsingExpressionAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD107AwaitTaskWithinUsingExpressionAnalyzerTests.cs @@ -1,18 +1,14 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System.Threading.Tasks; - using Xunit; - using Verify = CSharpCodeFixVerifier; +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; - public class VSTHRD107AwaitTaskWithinUsingExpressionAnalyzerTests +public class VSTHRD107AwaitTaskWithinUsingExpressionAnalyzerTests +{ + [Fact] + public async Task UsingTaskOfTReturningMethodInSyncMethod_GeneratesError() { - [Fact] - public async Task UsingTaskOfTReturningMethodInSyncMethod_GeneratesError() - { - var test = @" + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -25,7 +21,7 @@ void F() { } } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -39,14 +35,14 @@ async Task FAsync() { } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithSpan(8, 16, 8, 32); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithSpan(8, 16, 8, 32); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task UsingTaskOfTReturningMethodInIntReturningMethod_GeneratesError() - { - var test = @" + [Fact] + public async Task UsingTaskOfTReturningMethodInIntReturningMethod_GeneratesError() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -61,7 +57,7 @@ int F() { } } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -77,14 +73,14 @@ async Task FAsync() { } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithSpan(8, 16, 8, 32); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithSpan(8, 16, 8, 32); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task UsingTaskOfTReturningMethodInTaskReturningMethod_GeneratesError() - { - var test = @" + [Fact] + public async Task UsingTaskOfTReturningMethodInTaskReturningMethod_GeneratesError() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -99,7 +95,7 @@ Task F() { } } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -113,14 +109,14 @@ async Task F() { } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithSpan(8, 16, 8, 32); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithSpan(8, 16, 8, 32); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task UsingTaskOfTReturningMethodInAsyncMethod_GeneratesError() - { - var test = @" + [Fact] + public async Task UsingTaskOfTReturningMethodInAsyncMethod_GeneratesError() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -133,7 +129,7 @@ async Task F() { } } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -147,14 +143,14 @@ async Task F() { } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithSpan(8, 16, 8, 32); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithSpan(8, 16, 8, 32); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task UsingTaskOfTCompoundExpressionInAsyncMethod_GeneratesError() - { - var test = @" + [Fact] + public async Task UsingTaskOfTCompoundExpressionInAsyncMethod_GeneratesError() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -168,7 +164,7 @@ async Task F() { } } "; - var withFix = @" + var withFix = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -183,14 +179,14 @@ async Task F() { } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithSpan(9, 16, 9, 24); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithSpan(9, 16, 9, 24); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task UsingAwaitTaskOfTReturningMethod_GeneratesNoError() - { - var test = @" + [Fact] + public async Task UsingAwaitTaskOfTReturningMethod_GeneratesNoError() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -204,13 +200,13 @@ async Task F() { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task UsingAwaitTaskOfTask_GeneratesError() - { - var test = @" + [Fact] + public async Task UsingAwaitTaskOfTask_GeneratesError() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -225,14 +221,14 @@ async Task F() { } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithSpan(9, 16, 9, 27); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithSpan(9, 16, 9, 27); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task UsingTaskOfTLocal_GeneratesError() - { - var test = @" + [Fact] + public async Task UsingTaskOfTLocal_GeneratesError() + { + var test = @" using System; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -247,8 +243,7 @@ void F() { } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithSpan(9, 16, 9, 19); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithSpan(9, 16, 9, 19); + await CSVerify.VerifyAnalyzerAsync(test, expected); } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD108AssertThreadRequirementUnconditionallyTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD108AssertThreadRequirementUnconditionallyTests.cs index b87af79f2..da099f90e 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD108AssertThreadRequirementUnconditionallyTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD108AssertThreadRequirementUnconditionallyTests.cs @@ -1,18 +1,14 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System.Threading.Tasks; - using Xunit; - using Verify = CSharpCodeFixVerifier; +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; - public class VSTHRD108AssertThreadRequirementUnconditionallyTests +public class VSTHRD108AssertThreadRequirementUnconditionallyTests +{ + [Fact] + public async Task AffinityAssertion_Unconditional_ProducesNoDiagnostic() { - [Fact] - public async Task AffinityAssertion_Unconditional_ProducesNoDiagnostic() - { - var test = @" + var test = @" using System; using Microsoft.VisualStudio.Shell; @@ -22,13 +18,13 @@ void F() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task AffinityAssertion_WithinIfBlock_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task AffinityAssertion_WithinIfBlock_ProducesDiagnostic() + { + var test = @" using System; using Microsoft.VisualStudio.Shell; @@ -43,14 +39,14 @@ void F() { } } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithLocation(0); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(0); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task AffinityAssertion_WithinDelegateHostedWithinIfBlock_ProducesNoDiagnostic() - { - var test = @" + [Fact] + public async Task AffinityAssertion_WithinDelegateHostedWithinIfBlock_ProducesNoDiagnostic() + { + var test = @" using System; using Microsoft.VisualStudio.Shell; @@ -65,13 +61,13 @@ void F() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task AffinityAssertion_WithinIfBlockWithinDelegate_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task AffinityAssertion_WithinIfBlockWithinDelegate_ProducesDiagnostic() + { + var test = @" using System; using Microsoft.VisualStudio.Shell; @@ -89,14 +85,14 @@ void F() { } } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithLocation(0); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(0); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task AffinityAssertion_WithinWhileBlock_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task AffinityAssertion_WithinWhileBlock_ProducesDiagnostic() + { + var test = @" using System; using Microsoft.VisualStudio.Shell; @@ -111,14 +107,14 @@ void F() { } } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithLocation(0); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(0); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task AffinityAssertion_WithinForBlock_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task AffinityAssertion_WithinForBlock_ProducesDiagnostic() + { + var test = @" using System; using Microsoft.VisualStudio.Shell; @@ -133,14 +129,14 @@ void F() { } } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithLocation(0); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(0); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task AffinityAssertion_WithinDoWhileBlock_ProducesNoDiagnostic() - { - var test = @" + [Fact] + public async Task AffinityAssertion_WithinDoWhileBlock_ProducesNoDiagnostic() + { + var test = @" using System; using Microsoft.VisualStudio.Shell; @@ -154,13 +150,13 @@ void F() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task AffinityAssertion_WithinDebugAssert_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task AffinityAssertion_WithinDebugAssert_ProducesDiagnostic() + { + var test = @" using System; using System.Diagnostics; using Microsoft.VisualStudio.Shell; @@ -171,14 +167,14 @@ void F() { } } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithLocation(0); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(0); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task AffinityAssertion_WithinAnyConditionalMethodArg_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task AffinityAssertion_WithinAnyConditionalMethodArg_ProducesDiagnostic() + { + var test = @" using System; using System.Diagnostics; using Microsoft.VisualStudio.Shell; @@ -195,14 +191,14 @@ private void ThrowIfNot(bool expr) } } "; - CodeAnalysis.Testing.DiagnosticResult expected = Verify.Diagnostic().WithLocation(0); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = CSVerify.Diagnostic().WithLocation(0); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } - [Fact] - public async Task ThreadCheckWithinIfExpression_ProducesNoDiagnostic() - { - var test = @" + [Fact] + public async Task ThreadCheckWithinIfExpression_ProducesNoDiagnostic() + { + var test = @" using System; using Microsoft.VisualStudio.Shell; @@ -216,7 +212,6 @@ void F() { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD109AvoidAssertInAsyncMethodsAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD109AvoidAssertInAsyncMethodsAnalyzerTests.cs index 2df7509ba..b9f2ea635 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD109AvoidAssertInAsyncMethodsAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD109AvoidAssertInAsyncMethodsAnalyzerTests.cs @@ -1,18 +1,14 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System.Threading.Tasks; - using Xunit; - using Verify = CSharpCodeFixVerifier; +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; - public class VSTHRD109AvoidAssertInAsyncMethodsAnalyzerTests +public class VSTHRD109AvoidAssertInAsyncMethodsAnalyzerTests +{ + [Fact] + public async Task AsyncMethodAsserts_GeneratesDiagnostic() { - [Fact] - public async Task AsyncMethodAsserts_GeneratesDiagnostic() - { - var test = @" + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Task = System.Threading.Tasks.Task; @@ -25,7 +21,7 @@ async Task FooAsync() { } "; - var fix = @" + var fix = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Task = System.Threading.Tasks.Task; @@ -38,13 +34,13 @@ async Task FooAsync() { } "; - await Verify.VerifyCodeFixAsync(test, fix); - } + await CSVerify.VerifyCodeFixAsync(test, fix); + } - [Fact] - public async Task AsyncMethodAsserts_CodeFixReusesCancellationToken() - { - var test = @" + [Fact] + public async Task AsyncMethodAsserts_CodeFixReusesCancellationToken() + { + var test = @" using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; @@ -58,7 +54,7 @@ async Task FooAsync(CancellationToken ct) { } "; - var fix = @" + var fix = @" using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; @@ -72,13 +68,13 @@ async Task FooAsync(CancellationToken ct) { } "; - await Verify.VerifyCodeFixAsync(test, fix); - } + await CSVerify.VerifyCodeFixAsync(test, fix); + } - [Fact] - public async Task TaskReturningNonAsyncMethodAsserts_GeneratesDiagnostic() - { - var test = @" + [Fact] + public async Task TaskReturningNonAsyncMethodAsserts_GeneratesDiagnostic() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Task = System.Threading.Tasks.Task; @@ -91,7 +87,7 @@ Task FooAsync() { } "; - var fix = @" + var fix = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Task = System.Threading.Tasks.Task; @@ -104,13 +100,13 @@ async Task FooAsync() { } "; - await Verify.VerifyCodeFixAsync(test, fix); - } + await CSVerify.VerifyCodeFixAsync(test, fix); + } - [Fact] - public async Task VoidNonAsyncMethodAsserts_GeneratesNoDiagnostic() - { - var test = @" + [Fact] + public async Task VoidNonAsyncMethodAsserts_GeneratesNoDiagnostic() + { + var test = @" using Microsoft.VisualStudio.Shell; class Test { @@ -120,13 +116,13 @@ void Foo() { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task VoidAnonymousFunctionInsideAsyncMethod_GeneratesNoDiagnostic() - { - var test = @" + [Fact] + public async Task VoidAnonymousFunctionInsideAsyncMethod_GeneratesNoDiagnostic() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Task = System.Threading.Tasks.Task; @@ -140,13 +136,13 @@ await Task.Run(delegate { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task AsyncAnonymousFunctionInsideVoidMethod_GeneratesDiagnostic() - { - var test = @" + [Fact] + public async Task AsyncAnonymousFunctionInsideVoidMethod_GeneratesDiagnostic() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Task = System.Threading.Tasks.Task; @@ -161,7 +157,7 @@ void Foo() { } "; - var fix = @" + var fix = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Task = System.Threading.Tasks.Task; @@ -176,13 +172,13 @@ void Foo() { } "; - await Verify.VerifyCodeFixAsync(test, fix); - } + await CSVerify.VerifyCodeFixAsync(test, fix); + } - [Fact] - public async Task AsyncAnonymousFunctionInsideVoidMethod_CodeFixReusesCancellationToken() - { - var test = @" + [Fact] + public async Task AsyncAnonymousFunctionInsideVoidMethod_CodeFixReusesCancellationToken() + { + var test = @" using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; @@ -198,7 +194,7 @@ void Foo(CancellationToken ct) { } "; - var fix = @" + var fix = @" using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; @@ -214,13 +210,13 @@ void Foo(CancellationToken ct) { } "; - await Verify.VerifyCodeFixAsync(test, fix); - } + await CSVerify.VerifyCodeFixAsync(test, fix); + } - [Fact] - public async Task TaskReturningLambdaInsideVoidMethod_GeneratesDiagnostic() - { - var test = @" + [Fact] + public async Task TaskReturningLambdaInsideVoidMethod_GeneratesDiagnostic() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Task = System.Threading.Tasks.Task; @@ -235,7 +231,7 @@ void Foo() { } "; - var fix = @" + var fix = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Task = System.Threading.Tasks.Task; @@ -250,13 +246,13 @@ void Foo() { } "; - await Verify.VerifyCodeFixAsync(test, fix); - } + await CSVerify.VerifyCodeFixAsync(test, fix); + } - [Fact] - public async Task TaskReturningLambdaInsideVoidMethod_NoTypeArg_GeneratesDiagnostic() - { - var test = @" + [Fact] + public async Task TaskReturningLambdaInsideVoidMethod_NoTypeArg_GeneratesDiagnostic() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Task = System.Threading.Tasks.Task; @@ -271,7 +267,7 @@ void Foo() { } "; - var fix = @" + var fix = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Task = System.Threading.Tasks.Task; @@ -286,13 +282,13 @@ void Foo() { } "; - await Verify.VerifyCodeFixAsync(test, fix); - } + await CSVerify.VerifyCodeFixAsync(test, fix); + } - [Fact] - public async Task IntReturningLambdaInsideVoidMethod_GeneratesNoDiagnostic() - { - var test = @" + [Fact] + public async Task IntReturningLambdaInsideVoidMethod_GeneratesNoDiagnostic() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Task = System.Threading.Tasks.Task; @@ -307,7 +303,6 @@ void Foo() { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD110ObserveResultOfAsyncCallsAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD110ObserveResultOfAsyncCallsAnalyzerTests.cs index d8c1600aa..c9fca9dd6 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD110ObserveResultOfAsyncCallsAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD110ObserveResultOfAsyncCallsAnalyzerTests.cs @@ -1,46 +1,61 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System.Threading.Tasks; - using Microsoft.CodeAnalysis.Testing; - using Xunit; - using Verify = CSharpCodeFixVerifier; +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; +using VerifyVB = Microsoft.VisualStudio.Threading.Analyzers.Tests.VisualBasicCodeFixVerifier; - public class VSTHRD110ObserveResultOfAsyncCallsAnalyzerTests +public class VSTHRD110ObserveResultOfAsyncCallsAnalyzerTests +{ + [Fact] + public async Task SyncMethod_ProducesDiagnostic() { - [Fact] - public async Task SyncMethod_ProducesDiagnostic() - { - var test = @" + var test = @" using System.Threading.Tasks; class Test { void Foo() { - BarAsync(); + [|BarAsync()|]; } Task BarAsync() => null; } "; - DiagnosticResult expected = this.CreateDiagnostic(7, 9, 8); - await Verify.VerifyAnalyzerAsync(test, expected); - } + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task SyncMethod_ProducesDiagnostic_VB() + { + var test = @" +Imports System.Threading.Tasks + +Class Test + Sub Foo + [|BarAsync()|] + End Sub + + Function BarAsync() As Task + Return Nothing + End Function +End Class +"; + + await VerifyVB.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task SyncDelegateWithinAsyncMethod_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task SyncDelegateWithinAsyncMethod_ProducesDiagnostic() + { + var test = @" using System.Threading.Tasks; class Test { async Task Foo() { await Task.Run(delegate { - BarAsync(); + [|BarAsync()|]; }); } @@ -48,14 +63,13 @@ await Task.Run(delegate { } "; - DiagnosticResult expected = this.CreateDiagnostic(8, 13, 8); - await Verify.VerifyAnalyzerAsync(test, expected); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task AssignToLocal_ProducesNoDiagnostic() - { - var test = @" + [Fact] + public async Task AssignToLocal_ProducesNoDiagnostic() + { + var test = @" using System.Threading.Tasks; class Test { @@ -68,13 +82,13 @@ void Foo() } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ForgetExtension_ProducesNoDiagnostic() - { - var test = @" + [Fact] + public async Task ForgetExtension_ProducesNoDiagnostic() + { + var test = @" using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; @@ -88,13 +102,13 @@ void Foo() } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task AssignToField_ProducesNoDiagnostic() - { - var test = @" + [Fact] + public async Task AssignToField_ProducesNoDiagnostic() + { + var test = @" using System.Threading.Tasks; class Test { @@ -109,13 +123,13 @@ void Foo() } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task PassToOtherMethod_ProducesNoDiagnostic() - { - var test = @" + [Fact] + public async Task PassToOtherMethod_ProducesNoDiagnostic() + { + var test = @" using System.Threading.Tasks; class Test { @@ -130,13 +144,13 @@ void OtherMethod(Task t) { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ReturnStatement_ProducesNoDiagnostic() - { - var test = @" + [Fact] + public async Task ReturnStatement_ProducesNoDiagnostic() + { + var test = @" using System.Threading.Tasks; class Test { @@ -151,19 +165,19 @@ void OtherMethod(Task t) { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ContinueWith_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task ContinueWith_ProducesDiagnostic() + { + var test = @" using System.Threading.Tasks; class Test { void Foo() { - BarAsync().ContinueWith(_ => { }); // ContinueWith returns the dropped task + [|BarAsync().ContinueWith(_ => { })|]; // ContinueWith returns the dropped task } Task BarAsync() => null; @@ -172,14 +186,13 @@ void OtherMethod(Task t) { } } "; - DiagnosticResult expected = this.CreateDiagnostic(7, 20, 12); - await Verify.VerifyAnalyzerAsync(test, expected); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task AsyncMethod_ProducesNoDiagnostic() - { - var test = @" + [Fact] + public async Task AsyncMethod_ProducesNoDiagnostic() + { + var test = @" using System.Threading.Tasks; class Test { @@ -192,13 +205,33 @@ async Task FooAsync() } "; - await Verify.VerifyAnalyzerAsync(test); // CS4014 should already take care of this case. - } + await CSVerify.VerifyAnalyzerAsync(test); // CS4014 should already take care of this case. + } + + [Fact] + public async Task AsyncMethod_ProducesNoDiagnostic_VB() + { + var test = @" +Imports System.Threading.Tasks + +Class Test + Async Function Foo() As Task + BarAsync() + End Function + + Function BarAsync() As Task + Return Nothing + End Function +End Class +"; + + await VerifyVB.VerifyAnalyzerAsync(test); // CS4014 should already take care of this case. + } - [Fact] - public async Task CallToNonExistentMethod() - { - var test = @" + [Fact] + public async Task CallToNonExistentMethod() + { + var test = @" using System; class Test { @@ -210,61 +243,86 @@ void Bar() { } } "; - DiagnosticResult expected = DiagnosticResult.CompilerError("CS0103").WithLocation(6, 9).WithArguments("a"); - await Verify.VerifyAnalyzerAsync(test, expected); - } + DiagnosticResult expected = DiagnosticResult.CompilerError("CS0103").WithLocation(6, 9).WithArguments("a"); + await CSVerify.VerifyAnalyzerAsync(test, expected); + } + + [Fact(Skip = "Won't fix")] + public async Task GetAwaiterWithIncompatibleParameters() + { + string test = /* lang=c#-test */ """ + using System; + using System.Collections.Generic; + using System.Runtime.CompilerServices; + using System.Threading.Tasks; + + class Test + { + void Foo() + { + var stack = new Stack<(int, int)>(); + stack.Pop(); + } + } + + internal static class Extensions + { + internal static TaskAwaiter<(T1, T2)> GetAwaiter(this (Task, Task) tasks) => throw new NotImplementedException(); + } + """; + + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ConfigureAwait_ProducesDiagnostics() - { - var test = @" + [Fact] + public async Task ConfigureAwait_ProducesDiagnostics() + { + var test = @" using System.Threading.Tasks; class Test { void Foo() { - BarAsync().ConfigureAwait(false); + [|BarAsync().ConfigureAwait(false)|]; } Task BarAsync() => Task.CompletedTask; } "; - DiagnosticResult expected = this.CreateDiagnostic(7, 20, nameof(Task.ConfigureAwait).Length); - await Verify.VerifyAnalyzerAsync(test, expected); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ConfigureAwaitGenerics_ProducesDiagnostics() - { - var test = @" + [Fact] + public async Task ConfigureAwaitGenerics_ProducesDiagnostics() + { + var test = @" using System.Threading.Tasks; class Test { void Foo() { - BarAsync().ConfigureAwait(false); + [|BarAsync().ConfigureAwait(false)|]; } Task BarAsync() => Task.FromResult(0); } "; - DiagnosticResult expected = this.CreateDiagnostic(7, 20, nameof(Task.ConfigureAwait).Length); - await Verify.VerifyAnalyzerAsync(test, expected); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task CustomAwaitable_ProducesDiagnostics() - { - var test = @" + [Fact] + public async Task CustomAwaitable_ProducesDiagnostics() + { + var test = @" using System; using System.Runtime.CompilerServices; class Test { void Foo() { - BarAsync(); + [|BarAsync()|]; } CustomTask BarAsync() => new CustomTask(); @@ -288,14 +346,13 @@ public void GetResult() } } "; - DiagnosticResult expected = this.CreateDiagnostic(8, 9, 8); - await Verify.VerifyAnalyzerAsync(test, expected); - } - - [Fact] - public async Task CustomAwaitableLikeType_ProducesNoDiagnostic() - { - var test = @" + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task CustomAwaitableLikeType_ProducesNoDiagnostic() + { + var test = @" using System; class Test { @@ -320,73 +377,70 @@ class NotAwaitable } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task SyncMethodWithValueTask_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task SyncMethodWithValueTask_ProducesDiagnostic() + { + var test = @" using System.Threading.Tasks; class Test { void Foo() { - BarAsync(); + [|BarAsync()|]; } ValueTask BarAsync() => default; } "; - DiagnosticResult expected = this.CreateDiagnostic(7, 9, 8); - await Verify.VerifyAnalyzerAsync(test, expected); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ConfigureAwaitValueTask_ProducesDiagnostics() - { - var test = @" + [Fact] + public async Task ConfigureAwaitValueTask_ProducesDiagnostics() + { + var test = @" using System.Threading.Tasks; class Test { void Foo() { - BarAsync().ConfigureAwait(false); + [|BarAsync().ConfigureAwait(false)|]; } ValueTask BarAsync() => default; } "; - DiagnosticResult expected = this.CreateDiagnostic(7, 20, nameof(Task.ConfigureAwait).Length); - await Verify.VerifyAnalyzerAsync(test, expected); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ConditionalAccess_ProducesDiagnostic() - { - var test = @" + [Fact] + public async Task ConditionalAccess_ProducesDiagnostic() + { + var test = @" using System.Threading.Tasks; class Test { void Foo(Test? tester) { - tester?.BarAsync(); + tester?[|.BarAsync()|]; } Task BarAsync() => null; } "; - DiagnosticResult expected = this.CreateDiagnostic(7, 17, 8); - await Verify.VerifyAnalyzerAsync(test, expected); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ConditionalAccessAwaited_ProducesNoDiagnostic() - { - var test = @" + [Fact] + public async Task ConditionalAccessAwaited_ProducesNoDiagnostic() + { + var test = @" using System.Threading.Tasks; class Test { @@ -399,10 +453,247 @@ async Task Foo(Test? tester) } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task NullCoalescing_ProducesNoDiagnostic() + { + string test = """ + using System.Threading.Tasks; + + class Tree { + static Task ShakeTreeAsync(Tree? tree) => tree?.ShakeAsync() ?? Task.CompletedTask; + Task ShakeAsync() => Task.CompletedTask; + } + """; + + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task TaskInFinalizer() + { + string test = @" +using System; +using System.Threading.Tasks; + +public class Test : IAsyncDisposable +{ +~Test() +{ + [|Task.Run(async () => await DisposeAsync().ConfigureAwait(false))|]; +} + +public async ValueTask DisposeAsync() +{ + await Task.Delay(5000); +} +} +"; + + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task ParentheticalUseOfTaskResult_ProducesNoDiagnostic() + { + string test = """ + using System; + using System.Threading.Tasks; + + class Class1 + { + public Func>? VCLoadMethod; + + public int? VirtualCurrencyBalances => (VCLoadMethod?.Invoke()).GetAwaiter().GetResult(); + } + """; + + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task LocalVoidFunctionWithinAsyncTaskMethod() + { + string test = /* lang=c#-test */ """ + using System.Threading.Tasks; + + class Test + { + async Task DoOperationAsync() + { + DoOperationInner(); + + void DoOperationInner() + { + [|HelperAsync()|]; + } + } + + void DoOperation() + { + [|HelperAsync()|]; + } + + Task HelperAsync() => Task.CompletedTask; + } + """; + + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task ExpressionLambda_ProducesNoDiagnostic() + { + string test = """ + using System; + using System.Linq.Expressions; + using System.Threading.Tasks; + + interface ILogger + { + Task InfoAsync(string message); + } + + class MockVerifier + { + public static void Verify(Expression> expression) + { + } + } + + class Test + { + void TestMethod() + { + var logger = new MockLogger(); + MockVerifier.Verify(x => x.InfoAsync("test")); + } + } + + class MockLogger : ILogger + { + public Task InfoAsync(string message) => Task.CompletedTask; + } + """; + + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task ExpressionFuncLambda_ProducesNoDiagnostic() + { + string test = """ + using System; + using System.Linq.Expressions; + using System.Threading.Tasks; + + class Test + { + void TestMethod() + { + SomeMethod(x => x.InfoAsync("test")); + } + + void SomeMethod(Expression> expression) + { + } + + Task InfoAsync(string message) => Task.CompletedTask; + } + + interface ILogger + { + Task InfoAsync(string message); + } + """; + + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task MoqLikeScenario_ProducesNoDiagnostic() + { + string test = """ + using System; + using System.Linq.Expressions; + using System.Threading.Tasks; + + interface ILogger + { + Task InfoAsync(string message); + } + + class Mock + { + public void Verify(Expression> expression, Times times, string message) + { + } + } + + enum Times + { + Never + } + + class Test + { + void TestMethod() + { + var mock = new Mock(); + mock.Verify(x => x.InfoAsync("test"), Times.Never, "No Log should have been written"); + } + } + """; + + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task DirectTaskCall_StillProducesDiagnostic() + { + string test = """ + using System.Threading.Tasks; + + class Test + { + void TestMethod() + { + // This should still trigger VSTHRD110 - direct call not in expression + [|TaskReturningMethod()|]; + } + + Task TaskReturningMethod() => Task.CompletedTask; + } + """; + + await CSVerify.VerifyAnalyzerAsync(test); + } - private DiagnosticResult CreateDiagnostic(int line, int column, int length) - => Verify.Diagnostic().WithSpan(line, column, line, column + length); + [Fact] + public async Task ExpressionAssignment_ProducesNoDiagnostic() + { + string test = """ + using System; + using System.Linq.Expressions; + using System.Threading.Tasks; + + interface ILogger + { + Task InfoAsync(string message); + } + + class Test + { + void TestMethod() + { + // Assignment to Expression<> variable should not trigger VSTHRD110 + Expression> expr = x => x.InfoAsync("test"); + } + } + """; + + await CSVerify.VerifyAnalyzerAsync(test); } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD111UseConfigureAwaitAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD111UseConfigureAwaitAnalyzerTests.cs index 166844e92..4bac72047 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD111UseConfigureAwaitAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD111UseConfigureAwaitAnalyzerTests.cs @@ -1,19 +1,14 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System.Threading.Tasks; - using Microsoft.CodeAnalysis.Testing; - using Xunit; - using Verify = CSharpCodeFixVerifier; +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; - public class VSTHRD111UseConfigureAwaitAnalyzerTests +public class VSTHRD111UseConfigureAwaitAnalyzerTests +{ + [Fact] + public async Task AwaitOnTask_NoSuffix_GeneratesDiagnostic() { - [Fact] - public async Task AwaitOnTask_NoSuffix_GeneratesDiagnostic() - { - var test = @" + var test = @" using System.Threading.Tasks; class Test { @@ -25,7 +20,7 @@ async Task Foo() Task BarAsync() => default; } "; - var fixFalse = @" + var fixFalse = @" using System.Threading.Tasks; class Test { @@ -37,7 +32,7 @@ async Task Foo() Task BarAsync() => default; } "; - var fixTrue = @" + var fixTrue = @" using System.Threading.Tasks; class Test { @@ -50,24 +45,24 @@ async Task Foo() } "; - await new Verify.Test - { - TestCode = test, - FixedCode = fixFalse, - CodeActionEquivalenceKey = false.ToString(), - }.RunAsync(); - await new Verify.Test - { - TestCode = test, - FixedCode = fixTrue, - CodeActionEquivalenceKey = true.ToString(), - }.RunAsync(); - } - - [Fact] - public async Task AwaitOnValueTask_NoSuffix_GeneratesDiagnostic() + await new CSVerify.Test + { + TestCode = test, + FixedCode = fixFalse, + CodeActionEquivalenceKey = false.ToString(), + }.RunAsync(); + await new CSVerify.Test { - var test = @" + TestCode = test, + FixedCode = fixTrue, + CodeActionEquivalenceKey = true.ToString(), + }.RunAsync(); + } + + [Fact] + public async Task AwaitOnValueTask_NoSuffix_GeneratesDiagnostic() + { + var test = @" using System.Threading.Tasks; class Test { @@ -79,7 +74,7 @@ async Task Foo() ValueTask BarAsync() => default; } "; - var fixFalse = @" + var fixFalse = @" using System.Threading.Tasks; class Test { @@ -91,7 +86,7 @@ async Task Foo() ValueTask BarAsync() => default; } "; - var fixTrue = @" + var fixTrue = @" using System.Threading.Tasks; class Test { @@ -104,24 +99,24 @@ async Task Foo() } "; - await new Verify.Test - { - TestCode = test, - FixedCode = fixFalse, - CodeActionEquivalenceKey = false.ToString(), - }.RunAsync(); - await new Verify.Test - { - TestCode = test, - FixedCode = fixTrue, - CodeActionEquivalenceKey = true.ToString(), - }.RunAsync(); - } - - [Fact] - public async Task AwaitOnTaskOfT_NoSuffix_GeneratesDiagnostic() + await new CSVerify.Test { - var test = @" + TestCode = test, + FixedCode = fixFalse, + CodeActionEquivalenceKey = false.ToString(), + }.RunAsync(); + await new CSVerify.Test + { + TestCode = test, + FixedCode = fixTrue, + CodeActionEquivalenceKey = true.ToString(), + }.RunAsync(); + } + + [Fact] + public async Task AwaitOnTaskOfT_NoSuffix_GeneratesDiagnostic() + { + var test = @" using System.Threading.Tasks; class Test { @@ -133,7 +128,7 @@ async Task Foo() Task BarAsync() => default; } "; - var fixFalse = @" + var fixFalse = @" using System.Threading.Tasks; class Test { @@ -145,7 +140,7 @@ async Task Foo() Task BarAsync() => default; } "; - var fixTrue = @" + var fixTrue = @" using System.Threading.Tasks; class Test { @@ -158,18 +153,17 @@ async Task Foo() } "; - await new Verify.Test - { - TestCode = test, - FixedCode = fixFalse, - CodeActionEquivalenceKey = false.ToString(), - }.RunAsync(); - await new Verify.Test - { - TestCode = test, - FixedCode = fixTrue, - CodeActionEquivalenceKey = true.ToString(), - }.RunAsync(); - } + await new CSVerify.Test + { + TestCode = test, + FixedCode = fixFalse, + CodeActionEquivalenceKey = false.ToString(), + }.RunAsync(); + await new CSVerify.Test + { + TestCode = test, + FixedCode = fixTrue, + CodeActionEquivalenceKey = true.ToString(), + }.RunAsync(); } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD112ImplementSystemIAsyncDisposableAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD112ImplementSystemIAsyncDisposableAnalyzerTests.cs index eca49cfd7..7e2ac40cf 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD112ImplementSystemIAsyncDisposableAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD112ImplementSystemIAsyncDisposableAnalyzerTests.cs @@ -1,31 +1,27 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System.Threading.Tasks; - using Xunit; - using VBVerify = VisualBasicCodeFixVerifier; - using Verify = CSharpCodeFixVerifier; +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; +using VBVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.VisualBasicCodeFixVerifier; - public class VSTHRD112ImplementSystemIAsyncDisposableAnalyzerTests - { - private const string Preamble = @" +public class VSTHRD112ImplementSystemIAsyncDisposableAnalyzerTests +{ + private const string Preamble = @" using System.Threading.Tasks; using BclAsyncDisposable = System.IAsyncDisposable; using VsThreadingAsyncDisposable = Microsoft.VisualStudio.Threading.IAsyncDisposable; "; - private const string VBPreamble = @" + private const string VBPreamble = @" Imports System.Threading.Tasks Imports BclAsyncDisposable = System.IAsyncDisposable Imports VsThreadingAsyncDisposable = Microsoft.VisualStudio.Threading.IAsyncDisposable "; - [Fact] - public async Task ClassImplementsBoth() - { - var test = Preamble + @" + [Fact] + public async Task ClassImplementsBoth() + { + var test = Preamble + @" class Test : BclAsyncDisposable, VsThreadingAsyncDisposable { Task Microsoft.VisualStudio.Threading.IAsyncDisposable.DisposeAsync() @@ -38,25 +34,25 @@ Task Microsoft.VisualStudio.Threading.IAsyncDisposable.DisposeAsync() ValueTask System.IAsyncDisposable.DisposeAsync() => default; }"; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ClassImplementsOnlyBclType() - { - var test = Preamble + @" + [Fact] + public async Task ClassImplementsOnlyBclType() + { + var test = Preamble + @" class Test : BclAsyncDisposable { ValueTask System.IAsyncDisposable.DisposeAsync() => default; }"; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task ClassImplementsOnlyVsThreadingType() - { - var test = Preamble + @" + [Fact] + public async Task ClassImplementsOnlyVsThreadingType() + { + var test = Preamble + @" class Test : [|VsThreadingAsyncDisposable|] { public async Task DisposeAsync() @@ -65,7 +61,7 @@ public async Task DisposeAsync() } }"; - var fix = Preamble + @" + var fix = Preamble + @" class Test : VsThreadingAsyncDisposable, BclAsyncDisposable { public async Task DisposeAsync() @@ -79,13 +75,13 @@ ValueTask BclAsyncDisposable.DisposeAsync() } }"; - await Verify.VerifyCodeFixAsync(test, fix); - } + await CSVerify.VerifyCodeFixAsync(test, fix); + } - [Fact] - public async Task ClassImplementsOnlyVsThreadingType_WithBaseClassToo() - { - var test = Preamble + @" + [Fact] + public async Task ClassImplementsOnlyVsThreadingType_WithBaseClassToo() + { + var test = Preamble + @" class Test : object, [|VsThreadingAsyncDisposable|] { public async Task DisposeAsync() @@ -94,7 +90,7 @@ public async Task DisposeAsync() } }"; - var fix = Preamble + @" + var fix = Preamble + @" class Test : object, VsThreadingAsyncDisposable, BclAsyncDisposable { public async Task DisposeAsync() @@ -108,18 +104,18 @@ ValueTask BclAsyncDisposable.DisposeAsync() } }"; - await Verify.VerifyCodeFixAsync(test, fix); - } + await CSVerify.VerifyCodeFixAsync(test, fix); + } - [Fact] - public async Task ClassImplementsOnlyVsThreadingType_WithBaseClassToo_PartialClass() - { - var source1 = Preamble + @" + [Fact] + public async Task ClassImplementsOnlyVsThreadingType_WithBaseClassToo_PartialClass() + { + var source1 = Preamble + @" partial class Test { }"; - var source2 = Preamble + @" + var source2 = Preamble + @" partial class Test : [|VsThreadingAsyncDisposable|] { public async Task DisposeAsync() @@ -128,12 +124,12 @@ public async Task DisposeAsync() } }"; - var fix1 = Preamble + @" + var fix1 = Preamble + @" partial class Test { }"; - var fix2 = Preamble + @" + var fix2 = Preamble + @" partial class Test : VsThreadingAsyncDisposable, BclAsyncDisposable { public async Task DisposeAsync() @@ -147,19 +143,19 @@ ValueTask BclAsyncDisposable.DisposeAsync() } }"; - var test = new Verify.Test - { - TestState = { Sources = { source1, source2 } }, - FixedState = { Sources = { fix1, fix2 } }, - }; + var test = new CSVerify.Test + { + TestState = { Sources = { source1, source2 } }, + FixedState = { Sources = { fix1, fix2 } }, + }; - await test.RunAsync(); - } + await test.RunAsync(); + } - [Fact] - public async Task ClassImplementsOnlyVsThreadingType_WithBaseClassToo_VB() - { - var test = VBPreamble + @" + [Fact] + public async Task ClassImplementsOnlyVsThreadingType_WithBaseClassToo_VB() + { + var test = VBPreamble + @" Class Test Inherits Object Implements [|VsThreadingAsyncDisposable|] @@ -169,7 +165,7 @@ Await Task.Yield End Function End Class"; - var fix = VBPreamble + @" + var fix = VBPreamble + @" Class Test Inherits Object Implements VsThreadingAsyncDisposable, BclAsyncDisposable @@ -183,17 +179,17 @@ Return New ValueTask(DisposeAsync()) End Function End Class"; - await VBVerify.VerifyCodeFixAsync(test, fix); - } + await VBVerify.VerifyCodeFixAsync(test, fix); + } - [Fact] - public async Task ClassImplementsOnlyVsThreadingType_WithBaseClassToo_PartialClass_VB() - { - var source1 = VBPreamble + @" + [Fact] + public async Task ClassImplementsOnlyVsThreadingType_WithBaseClassToo_PartialClass_VB() + { + var source1 = VBPreamble + @" Partial Class Test End Class"; - var source2 = VBPreamble + @" + var source2 = VBPreamble + @" Partial Class Test Inherits Object Implements [|VsThreadingAsyncDisposable|] @@ -203,11 +199,11 @@ Await Task.Yield End Function End Class"; - var fix1 = VBPreamble + @" + var fix1 = VBPreamble + @" Partial Class Test End Class"; - var fix2 = VBPreamble + @" + var fix2 = VBPreamble + @" Partial Class Test Inherits Object Implements VsThreadingAsyncDisposable, BclAsyncDisposable @@ -221,19 +217,19 @@ Return New ValueTask(DisposeAsync()) End Function End Class"; - var test = new VBVerify.Test - { - TestState = { Sources = { source1, source2 } }, - FixedState = { Sources = { fix1, fix2 } }, - }; + var test = new VBVerify.Test + { + TestState = { Sources = { source1, source2 } }, + FixedState = { Sources = { fix1, fix2 } }, + }; - await test.RunAsync(); - } + await test.RunAsync(); + } - [Fact] - public async Task StructImplementsBoth() - { - var test = Preamble + @" + [Fact] + public async Task StructImplementsBoth() + { + var test = Preamble + @" struct Test : BclAsyncDisposable, VsThreadingAsyncDisposable { public Task DisposeAsync() @@ -249,25 +245,25 @@ ValueTask BclAsyncDisposable.DisposeAsync() } }"; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task StructImplementsOnlyBclType() - { - var test = Preamble + @" + [Fact] + public async Task StructImplementsOnlyBclType() + { + var test = Preamble + @" struct Test : BclAsyncDisposable { ValueTask System.IAsyncDisposable.DisposeAsync() => default; }"; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task StructImplementsOnlyVsThreadingType() - { - var test = Preamble + @" + [Fact] + public async Task StructImplementsOnlyVsThreadingType() + { + var test = Preamble + @" struct Test : [|VsThreadingAsyncDisposable|] { public async Task DisposeAsync() @@ -276,7 +272,7 @@ public async Task DisposeAsync() } }"; - var fix = Preamble + @" + var fix = Preamble + @" struct Test : VsThreadingAsyncDisposable, BclAsyncDisposable { public async Task DisposeAsync() @@ -290,13 +286,13 @@ ValueTask BclAsyncDisposable.DisposeAsync() } }"; - await Verify.VerifyCodeFixAsync(test, fix); - } + await CSVerify.VerifyCodeFixAsync(test, fix); + } - [Fact] - public async Task StructImplementsOnlyVsThreadingType_VB() - { - var test = VBPreamble + @" + [Fact] + public async Task StructImplementsOnlyVsThreadingType_VB() + { + var test = VBPreamble + @" Public Structure Test Implements [|VsThreadingAsyncDisposable|] @@ -305,7 +301,7 @@ Await Task.Yield End Function End Structure"; - var fix = VBPreamble + @" + var fix = VBPreamble + @" Public Structure Test Implements VsThreadingAsyncDisposable, BclAsyncDisposable @@ -318,24 +314,24 @@ Return New ValueTask(DisposeAsync()) End Function End Structure"; - await VBVerify.VerifyCodeFixAsync(test, fix); - } + await VBVerify.VerifyCodeFixAsync(test, fix); + } - [Fact] - public async Task InterfaceImplementsBoth() - { - var test = Preamble + @" + [Fact] + public async Task InterfaceImplementsBoth() + { + var test = Preamble + @" interface Test : BclAsyncDisposable, VsThreadingAsyncDisposable { }"; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task InterfaceImplementsBoth_AcrossTypeHierarchy() - { - var test = Preamble + @" + [Fact] + public async Task InterfaceImplementsBoth_AcrossTypeHierarchy() + { + var test = Preamble + @" interface Test1 : BclAsyncDisposable { } @@ -345,50 +341,49 @@ interface Test2 : Test1, VsThreadingAsyncDisposable } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task InterfaceImplementsOnlyBclType() - { - var test = Preamble + @" + [Fact] + public async Task InterfaceImplementsOnlyBclType() + { + var test = Preamble + @" interface Test : BclAsyncDisposable { }"; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task InterfaceImplementsOnlyVsThreadingType() - { - var test = Preamble + @" + [Fact] + public async Task InterfaceImplementsOnlyVsThreadingType() + { + var test = Preamble + @" interface Test : [|VsThreadingAsyncDisposable|] { }"; - var fix = Preamble + @" + var fix = Preamble + @" interface Test : VsThreadingAsyncDisposable, BclAsyncDisposable { }"; - await Verify.VerifyCodeFixAsync(test, fix); - } + await CSVerify.VerifyCodeFixAsync(test, fix); + } - [Fact] - public async Task InterfaceImplementsOnlyVsThreadingType_VB() - { - var test = VBPreamble + @" + [Fact] + public async Task InterfaceImplementsOnlyVsThreadingType_VB() + { + var test = VBPreamble + @" Interface ITest Inherits [|VsThreadingAsyncDisposable|] End Interface"; - var fix = VBPreamble + @" + var fix = VBPreamble + @" Interface ITest Inherits VsThreadingAsyncDisposable, BclAsyncDisposable End Interface"; - await VBVerify.VerifyCodeFixAsync(test, fix); - } + await VBVerify.VerifyCodeFixAsync(test, fix); } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD113CheckForSystemIAsyncDisposableAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD113CheckForSystemIAsyncDisposableAnalyzerTests.cs index 2548b5197..31ff51ed2 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD113CheckForSystemIAsyncDisposableAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD113CheckForSystemIAsyncDisposableAnalyzerTests.cs @@ -1,31 +1,27 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System.Threading.Tasks; - using Xunit; - using VBVerify = VisualBasicCodeFixVerifier; - using Verify = CSharpCodeFixVerifier; +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; +using VBVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.VisualBasicCodeFixVerifier; - public class VSTHRD113CheckForSystemIAsyncDisposableAnalyzerTests - { - private const string Preamble = @" +public class VSTHRD113CheckForSystemIAsyncDisposableAnalyzerTests +{ + private const string Preamble = @" using System.Threading.Tasks; using BclAsyncDisposable = System.IAsyncDisposable; using VsThreadingAsyncDisposable = Microsoft.VisualStudio.Threading.IAsyncDisposable; "; - private const string VBPreamble = @" + private const string VBPreamble = @" Imports System.Threading.Tasks Imports BclAsyncDisposable = System.IAsyncDisposable Imports VsThreadingAsyncDisposable = Microsoft.VisualStudio.Threading.IAsyncDisposable "; - [Fact] - public async Task MethodChecksBoth_WithIsCast() - { - var test = Preamble + @" + [Fact] + public async Task MethodChecksBoth_WithIsCast() + { + var test = Preamble + @" class Test { async Task CheckAndDispose(object o) { if (o is BclAsyncDisposable bcl) { @@ -37,13 +33,13 @@ async Task CheckAndDispose(object o) { } }"; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task MethodChecksBoth_WithIsCheck() - { - var test = Preamble + @" + [Fact] + public async Task MethodChecksBoth_WithIsCheck() + { + var test = Preamble + @" class Test { async Task CheckAndDispose(object o) { if (o is BclAsyncDisposable) { @@ -55,13 +51,13 @@ async Task CheckAndDispose(object o) { } }"; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task MethodChecksVsThreadingOnly_WithIsCheck() - { - var test = Preamble + @" + [Fact] + public async Task MethodChecksVsThreadingOnly_WithIsCheck() + { + var test = Preamble + @" class Test { async Task CheckAndDispose(object o) { if ([|o is VsThreadingAsyncDisposable|]) { @@ -70,13 +66,13 @@ async Task CheckAndDispose(object o) { } }"; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task MethodChecksVsThreadingOnly_WithIsCast() - { - var test = Preamble + @" + [Fact] + public async Task MethodChecksVsThreadingOnly_WithIsCast() + { + var test = Preamble + @" class Test { async Task CheckAndDispose(object o) { if ([|o is VsThreadingAsyncDisposable vs|]) { @@ -85,13 +81,13 @@ async Task CheckAndDispose(object o) { } }"; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact(Skip = "Too complex to support")] - public async Task MethodChecksVsThreadingOnly_WithIsCast_MultiBlock() - { - var test = Preamble + @" + [Fact(Skip = "Too complex to support")] + public async Task MethodChecksVsThreadingOnly_WithIsCast_MultiBlock() + { + var test = Preamble + @" class Test { async Task CheckAndDispose(object o, bool flag) { if (flag) { @@ -109,13 +105,13 @@ async Task CheckAndDispose(object o, bool flag) { } }"; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task MethodChecksBclOnly_WithIsCast() - { - var test = Preamble + @" + [Fact] + public async Task MethodChecksBclOnly_WithIsCast() + { + var test = Preamble + @" class Test { async Task CheckAndDispose(object o) { if (o is BclAsyncDisposable bcl) { @@ -124,13 +120,13 @@ async Task CheckAndDispose(object o) { } }"; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task MethodChecksBoth_WithAsCast() - { - var test = Preamble + @" + [Fact] + public async Task MethodChecksBoth_WithAsCast() + { + var test = Preamble + @" class Test { async Task CheckAndDispose(object o) { await ((o as BclAsyncDisposable)?.DisposeAsync() ?? default); @@ -138,26 +134,26 @@ async Task CheckAndDispose(object o) { } }"; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task MethodChecksVsThreadingOnly_WithAsCast() - { - var test = Preamble + @" + [Fact] + public async Task MethodChecksVsThreadingOnly_WithAsCast() + { + var test = Preamble + @" class Test { async Task CheckAndDispose(object o) { await (([|o as VsThreadingAsyncDisposable|])?.DisposeAsync() ?? Task.CompletedTask); } }"; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task MethodChecksBoth_WithTryCast_VB() - { - var test = VBPreamble + @" + [Fact] + public async Task MethodChecksBoth_WithTryCast_VB() + { + var test = VBPreamble + @" Class Test Async Function CheckAndDispose(o) As Task Dim bcl = TryCast(o, BclAsyncDisposable) @@ -171,13 +167,13 @@ End If End Function End Class"; - await VBVerify.VerifyAnalyzerAsync(test); - } + await VBVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task MethodChecksVsThreadingOnly_WithTypeOf_VB() - { - var test = VBPreamble + @" + [Fact] + public async Task MethodChecksVsThreadingOnly_WithTypeOf_VB() + { + var test = VBPreamble + @" Class Test Async Function CheckAndDispose(o) As Task If ([|TypeOf o Is VsThreadingAsyncDisposable|]) Then @@ -185,13 +181,13 @@ End If End Function End Class"; - await VBVerify.VerifyAnalyzerAsync(test); - } + await VBVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task MethodChecksVsThreadingOnly_WithTryCast_VB() - { - var test = VBPreamble + @" + [Fact] + public async Task MethodChecksVsThreadingOnly_WithTryCast_VB() + { + var test = VBPreamble + @" Class Test Async Function CheckAndDispose(o) As Task Dim vs = [|TryCast(o, VsThreadingAsyncDisposable)|] @@ -201,13 +197,13 @@ End If End Function End Class"; - await VBVerify.VerifyAnalyzerAsync(test); - } + await VBVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task MethodChecksVsThreadingOnly_WithCType_VB() - { - var test = VBPreamble + @" + [Fact] + public async Task MethodChecksVsThreadingOnly_WithCType_VB() + { + var test = VBPreamble + @" Class Test Async Function CheckAndDispose(o) As Task Dim vs = [|CType(o, VsThreadingAsyncDisposable)|] @@ -215,13 +211,13 @@ Await vs.DisposeAsync End Function End Class"; - await VBVerify.VerifyAnalyzerAsync(test); - } + await VBVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task MethodChecksBoth_WithCType_VB() - { - var test = VBPreamble + @" + [Fact] + public async Task MethodChecksBoth_WithCType_VB() + { + var test = VBPreamble + @" Class Test Async Function CheckAndDispose(o) As Task If (TypeOf o Is VsThreadingAsyncDisposable) Then @@ -234,13 +230,13 @@ End If End Function End Class"; - await VBVerify.VerifyAnalyzerAsync(test); - } + await VBVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task MethodChecksBoth_WithDirectCast_VB() - { - var test = VBPreamble + @" + [Fact] + public async Task MethodChecksBoth_WithDirectCast_VB() + { + var test = VBPreamble + @" Class Test Async Function CheckAndDispose(o) As Task If (TypeOf o Is VsThreadingAsyncDisposable) Then @@ -253,20 +249,19 @@ End If End Function End Class"; - await VBVerify.VerifyAnalyzerAsync(test); - } + await VBVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task MethodChecksBclOnly_WithAsCast() - { - var test = Preamble + @" + [Fact] + public async Task MethodChecksBclOnly_WithAsCast() + { + var test = Preamble + @" class Test { async Task CheckAndDispose(object o) { await ((o as BclAsyncDisposable)?.DisposeAsync() ?? default); } }"; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD114AvoidReturningNullTaskAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD114AvoidReturningNullTaskAnalyzerTests.cs index d11c22696..5487ddbe6 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD114AvoidReturningNullTaskAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD114AvoidReturningNullTaskAnalyzerTests.cs @@ -1,19 +1,15 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System.Threading.Tasks; - using Xunit; - using VerifyCS = CSharpCodeFixVerifier; - using VerifyVB = VisualBasicCodeFixVerifier; +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; +using VerifyVB = Microsoft.VisualStudio.Threading.Analyzers.Tests.VisualBasicCodeFixVerifier; - public class VSTHRD114AvoidReturningNullTaskAnalyzerTests +public class VSTHRD114AvoidReturningNullTaskAnalyzerTests +{ + [Fact] + public async Task TaskOfTReturnsNull_Diagnostic() { - [Fact] - public async Task TaskOfTReturnsNull_Diagnostic() - { - var csharpTest = @" + var csharpTest = @" using System.Threading.Tasks; class Test @@ -24,12 +20,12 @@ public Task GetTaskObj() } } "; - await new VerifyCS.Test - { - TestCode = csharpTest, - }.RunAsync(); + await new CSVerify.Test + { + TestCode = csharpTest, + }.RunAsync(); - var vbTest = @" + var vbTest = @" Imports System.Threading.Tasks Friend Class Test @@ -38,16 +34,16 @@ Return [|Nothing|] End Function End Class "; - await new VerifyVB.Test - { - TestCode = vbTest, - }.RunAsync(); - } - - [Fact] - public async Task TaskReturnsNull_Diagnostic() + await new VerifyVB.Test { - var csharpTest = @" + TestCode = vbTest, + }.RunAsync(); + } + + [Fact] + public async Task TaskReturnsNull_Diagnostic() + { + var csharpTest = @" using System.Threading.Tasks; class Test @@ -58,12 +54,12 @@ public Task GetTask() } } "; - await new VerifyCS.Test - { - TestCode = csharpTest, - }.RunAsync(); + await new CSVerify.Test + { + TestCode = csharpTest, + }.RunAsync(); - var vbTest = @" + var vbTest = @" Imports System.Threading.Tasks Friend Class Test @@ -72,16 +68,16 @@ Return [|Nothing|] End Function End Class "; - await new VerifyVB.Test - { - TestCode = vbTest, - }.RunAsync(); - } - - [Fact] - public async Task TaskArrowReturnsNull_Diagnostic() + await new VerifyVB.Test { - var test = @" + TestCode = vbTest, + }.RunAsync(); + } + + [Fact] + public async Task TaskArrowReturnsNull_Diagnostic() + { + var test = @" using System.Threading.Tasks; class Test @@ -89,16 +85,16 @@ class Test public Task GetTask() => [|null|]; } "; - await new VerifyCS.Test - { - TestCode = test, - }.RunAsync(); - } - - [Fact] - public async Task AsyncReturnsNull_NoDiagnostic() + await new CSVerify.Test { - var csharpTest = @" + TestCode = test, + }.RunAsync(); + } + + [Fact] + public async Task AsyncReturnsNull_NoDiagnostic() + { + var csharpTest = @" using System.Threading.Tasks; class Test @@ -109,12 +105,12 @@ public async Task GetTaskObj() } } "; - await new VerifyCS.Test - { - TestCode = csharpTest, - }.RunAsync(); + await new CSVerify.Test + { + TestCode = csharpTest, + }.RunAsync(); - var vbTest = @" + var vbTest = @" Imports System.Threading.Tasks Friend Class Test @@ -123,16 +119,16 @@ Return Nothing End Function End Class "; - await new VerifyVB.Test - { - TestCode = vbTest, - }.RunAsync(); - } - - [Fact] - public async Task VariableIsNullAndReturned_NoDiagnostic_FalseNegative() + await new VerifyVB.Test { - var test = @" + TestCode = vbTest, + }.RunAsync(); + } + + [Fact] + public async Task VariableIsNullAndReturned_NoDiagnostic_FalseNegative() + { + var test = @" using System.Threading.Tasks; class Test @@ -144,36 +140,77 @@ public Task GetTaskObj() } } "; - await new VerifyCS.Test - { - TestCode = test, - }.RunAsync(); - } - - [Fact] - public async Task NullInTernary_NoDiagnostic_FalseNegative() + await new CSVerify.Test { - var test = @" + TestCode = test, + }.RunAsync(); + } + + [Fact] + public async Task NullInTernary_Diagnostic() + { + var test = @" using System.Threading.Tasks; class Test { public Task GetTaskObj(bool b) { - return b ? default(Task) : null; + return b ? [|default(Task)|] : [|null|]; } } "; - await new VerifyCS.Test - { - TestCode = test, - }.RunAsync(); - } + await new CSVerify.Test + { + TestCode = test, + }.RunAsync(); + } + + [Fact] + public async Task NullInTernaryReturnStatement_Diagnostic() + { + var csharpTest = @" +using System.Threading.Tasks; + +class Test +{ + public Task First(bool flag) + { + return flag + ? [|null|] + : Task.CompletedTask; + } + + public Task Second(bool flag) => + flag + ? [|null|] + : Task.CompletedTask; +} +"; + await new CSVerify.Test + { + TestCode = csharpTest, + }.RunAsync(); - [Fact] - public async Task MultipleFaultyReturns_MultipleDiagnostics() + var vbTest = @" +Imports System.Threading.Tasks + +Friend Class Test + Public Function First(flag As Boolean) As Task + Return If(flag, [|Nothing|], Task.CompletedTask) + End Function +End Class +"; + await new VerifyVB.Test { - var test = @" + TestCode = vbTest, + }.RunAsync(); + } + + [Fact] + public async Task MultipleFaultyReturns_MultipleDiagnostics() + { + var test = @" using System.Threading.Tasks; class Test @@ -189,16 +226,16 @@ public Task GetTaskObj(string s) } } "; - await new VerifyCS.Test - { - TestCode = test, - }.RunAsync(); - } - - [Fact] - public async Task AsyncAnonymousDelegateReturnsNull_NoDiagnostic() + await new CSVerify.Test { - var test = @" + TestCode = test, + }.RunAsync(); + } + + [Fact] + public async Task AsyncAnonymousDelegateReturnsNull_NoDiagnostic() + { + var test = @" using System.Threading.Tasks; class Test @@ -212,16 +249,16 @@ public Task Foo() } } "; - await new VerifyCS.Test - { - TestCode = test, - }.RunAsync(); - } - - [Fact] - public async Task NonAsyncAnonymousDelegateReturnsNull_Diagnostic() + await new CSVerify.Test { - var test = @" + TestCode = test, + }.RunAsync(); + } + + [Fact] + public async Task NonAsyncAnonymousDelegateReturnsNull_Diagnostic() + { + var test = @" using System.Threading.Tasks; class Test @@ -235,16 +272,16 @@ public void Foo() } } "; - await new VerifyCS.Test - { - TestCode = test, - }.RunAsync(); - } - - [Fact] - public async Task LocalFunctionNonAsyncReturnsNull_Diagnostic() + await new CSVerify.Test { - var csharpTest = @" + TestCode = test, + }.RunAsync(); + } + + [Fact] + public async Task LocalFunctionNonAsyncReturnsNull_Diagnostic() + { + var csharpTest = @" using System.Threading.Tasks; class Test @@ -258,16 +295,16 @@ Task GetTaskObj() } } "; - await new VerifyCS.Test - { - TestCode = csharpTest, - }.RunAsync(); - } - - [Fact] - public async Task LocalFunctionAsyncReturnsNull_NoDiagnostic() + await new CSVerify.Test { - var csharpTest = @" + TestCode = csharpTest, + }.RunAsync(); + } + + [Fact] + public async Task LocalFunctionAsyncReturnsNull_NoDiagnostic() + { + var csharpTest = @" using System.Threading.Tasks; class Test @@ -281,16 +318,16 @@ async Task GetTaskObj() } } "; - await new VerifyCS.Test - { - TestCode = csharpTest, - }.RunAsync(); - } - - [Fact] - public async Task ReturnNullableTask_NoDiagnostic() + await new CSVerify.Test { - var csharpTest = @" + TestCode = csharpTest, + }.RunAsync(); + } + + [Fact] + public async Task ReturnNullableTask_NoDiagnostic() + { + var csharpTest = @" using System; using System.Threading.Tasks; @@ -320,10 +357,9 @@ public void LocalFunc() } } "; - await new VerifyCS.Test - { - TestCode = csharpTest, - }.RunAsync(); - } + await new CSVerify.Test + { + TestCode = csharpTest, + }.RunAsync(); } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD114AvoidReturningNullTaskCodeFixTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD114AvoidReturningNullTaskCodeFixTests.cs index 449bbe87c..0aba86b77 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD114AvoidReturningNullTaskCodeFixTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD114AvoidReturningNullTaskCodeFixTests.cs @@ -1,19 +1,15 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System.Threading.Tasks; - using Xunit; - using VerifyCS = CSharpCodeFixVerifier; - using VerifyVB = VisualBasicCodeFixVerifier; +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; +using VerifyVB = Microsoft.VisualStudio.Threading.Analyzers.Tests.VisualBasicCodeFixVerifier; - public class VSTHRD114AvoidReturningNullTaskCodeFixTests +public class VSTHRD114AvoidReturningNullTaskCodeFixTests +{ + [Fact] + public async Task MethodTaskOfTReturnsNull() { - [Fact] - public async Task MethodTaskOfTReturnsNull() - { - var test = @" + var test = @" class Test { public System.Threading.Tasks.Task GetTaskObj() @@ -22,7 +18,7 @@ public System.Threading.Tasks.Task GetTaskObj() } }"; - var withFix = @" + var withFix = @" class Test { public System.Threading.Tasks.Task GetTaskObj() @@ -31,33 +27,33 @@ public System.Threading.Tasks.Task GetTaskObj() } }"; - await VerifyCS.VerifyCodeFixAsync(test, withFix); - } + await CSVerify.VerifyCodeFixAsync(test, withFix); + } - [Fact] - public async Task MethodTaskOfTReturnsNothing_VB() - { - var test = @" + [Fact] + public async Task MethodTaskOfTReturnsNothing_VB() + { + var test = @" Class Test Function GetTaskObj As System.Threading.Tasks.Task(Of object) Return [|Nothing|] End Function End Class"; - var withFix = @" + var withFix = @" Class Test Function GetTaskObj As System.Threading.Tasks.Task(Of object) Return System.Threading.Tasks.Task.FromResult(Of Object)(Nothing) End Function End Class"; - await VerifyVB.VerifyCodeFixAsync(test, withFix); - } + await VerifyVB.VerifyCodeFixAsync(test, withFix); + } - [Fact] - public async Task MethodTaskReturnsNull() - { - var test = @" + [Fact] + public async Task MethodTaskReturnsNull() + { + var test = @" using System.Threading.Tasks; class Test @@ -68,7 +64,7 @@ public Task GetTask() } } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; class Test @@ -80,13 +76,13 @@ public Task GetTask() } "; - await VerifyCS.VerifyCodeFixAsync(test, withFix); - } + await CSVerify.VerifyCodeFixAsync(test, withFix); + } - [Fact] - public async Task ArrowedMethodTaskReturnsNull() - { - var test = @" + [Fact] + public async Task ArrowedMethodTaskReturnsNull() + { + var test = @" using System.Threading.Tasks; class Test @@ -94,7 +90,7 @@ class Test public Task GetTask() => [|null|]; } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; class Test @@ -103,13 +99,13 @@ class Test } "; - await VerifyCS.VerifyCodeFixAsync(test, withFix); - } + await CSVerify.VerifyCodeFixAsync(test, withFix); + } - [Fact] - public async Task ArrowedMethodTaskOfTReturnsNull() - { - var test = @" + [Fact] + public async Task ArrowedMethodTaskOfTReturnsNull() + { + var test = @" using System.Threading.Tasks; class Test @@ -117,7 +113,7 @@ class Test public Task GetTaskObj() => [|null|]; } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; class Test @@ -126,13 +122,13 @@ class Test } "; - await VerifyCS.VerifyCodeFixAsync(test, withFix); - } + await CSVerify.VerifyCodeFixAsync(test, withFix); + } - [Fact] - public async Task MultipleFaultyReturns() - { - var test = @" + [Fact] + public async Task MultipleFaultyReturns() + { + var test = @" using System.Threading.Tasks; class Test @@ -148,7 +144,7 @@ public Task GetTaskObj(string s) } } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; class Test @@ -165,13 +161,13 @@ public Task GetTaskObj(string s) } "; - await VerifyCS.VerifyCodeFixAsync(test, withFix); - } + await CSVerify.VerifyCodeFixAsync(test, withFix); + } - [Fact] - public async Task MethodComplexTaskOfTReturnsNull() - { - var test = @" + [Fact] + public async Task MethodComplexTaskOfTReturnsNull() + { + var test = @" using System.Collections.Generic; using System.Threading.Tasks; @@ -183,7 +179,7 @@ public Task>> GetTaskObj() } }"; - var withFix = @" + var withFix = @" using System.Collections.Generic; using System.Threading.Tasks; @@ -195,13 +191,13 @@ public Task>> GetTaskObj() } }"; - await VerifyCS.VerifyCodeFixAsync(test, withFix); - } + await CSVerify.VerifyCodeFixAsync(test, withFix); + } - [Fact] - public async Task AnonymousDelegateTaskOfTReturnsNull() - { - var test = @" + [Fact] + public async Task AnonymousDelegateTaskOfTReturnsNull() + { + var test = @" using System.Threading.Tasks; class Test @@ -216,7 +212,7 @@ public void Foo() } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; class Test @@ -230,13 +226,13 @@ public void Foo() } } "; - await VerifyCS.VerifyCodeFixAsync(test, withFix); - } + await CSVerify.VerifyCodeFixAsync(test, withFix); + } - [Fact] - public async Task LambdaTaskOfTReturnsNull() - { - var test = @" + [Fact] + public async Task LambdaTaskOfTReturnsNull() + { + var test = @" using System.Threading.Tasks; class Test @@ -251,7 +247,7 @@ public void Foo() } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; class Test @@ -265,12 +261,13 @@ public void Foo() } } "; - await VerifyCS.VerifyCodeFixAsync(test, withFix); - } + await CSVerify.VerifyCodeFixAsync(test, withFix); + } - public async Task AnonymousDelegateTaskReturnsNull() - { - var test = @" + [Fact] + public async Task AnonymousDelegateTaskReturnsNull() + { + var test = @" using System.Threading.Tasks; class Test @@ -285,7 +282,7 @@ public void Foo() } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; class Test @@ -299,12 +296,13 @@ public void Foo() } } "; - await VerifyCS.VerifyCodeFixAsync(test, withFix); - } + await CSVerify.VerifyCodeFixAsync(test, withFix); + } - public async Task LambdaTaskReturnsNull() - { - var test = @" + [Fact] + public async Task LambdaTaskReturnsNull() + { + var test = @" using System.Threading.Tasks; class Test @@ -319,7 +317,7 @@ public void Foo() } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; class Test @@ -333,7 +331,6 @@ public void Foo() } } "; - await VerifyCS.VerifyCodeFixAsync(test, withFix); - } + await CSVerify.VerifyCodeFixAsync(test, withFix); } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD115AvoidJoinableTaskContextCtorWithNullArgTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD115AvoidJoinableTaskContextCtorWithNullArgTests.cs new file mode 100644 index 000000000..8603eb8c9 --- /dev/null +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD115AvoidJoinableTaskContextCtorWithNullArgTests.cs @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; +using VBVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.VisualBasicCodeFixVerifier; + +public class VSTHRD115AvoidJoinableTaskContextCtorWithNullArgTests +{ + private const string CSPreamble = """ + using System.Threading; + using Microsoft.VisualStudio.Threading; + + """; + + private const string VBPreamble = """ + Imports System.Threading + Imports Microsoft.VisualStudio.Threading + + """; + + [Fact] + public async Task ConstructorWithImplicitNullSyncContext_SuppressWarning_CS() + { + var test = CSPreamble + """ + class Test + { + void Create1() => [|new JoinableTaskContext(null)|]; + void Create2() => [|new JoinableTaskContext(Thread.CurrentThread)|]; + } + """; + + var withFix = CSPreamble + """ + class Test + { + void Create1() => new JoinableTaskContext(null, SynchronizationContext.Current); + void Create2() => new JoinableTaskContext(Thread.CurrentThread, SynchronizationContext.Current); + } + """; + + await new CSVerify.Test + { + TestCode = test, + FixedCode = withFix, + CodeActionEquivalenceKey = VSTHRD115AvoidJoinableTaskContextCtorWithNullArgsCodeFix.SuppressWarningEquivalenceKey, + }.RunAsync(); + } + + [Fact] + public async Task ConstructorWithExplicitNullSyncContext_SuppressWarning() + { + var test = CSPreamble + """ + class Test + { + void Create1() => new JoinableTaskContext(null, [|null|]); + void Create2() => new JoinableTaskContext(Thread.CurrentThread, [|null|]); + } + """; + + var withFix = CSPreamble + """ + class Test + { + void Create1() => new JoinableTaskContext(null, SynchronizationContext.Current); + void Create2() => new JoinableTaskContext(Thread.CurrentThread, SynchronizationContext.Current); + } + """; + + await new CSVerify.Test + { + TestCode = test, + FixedCode = withFix, + CodeActionEquivalenceKey = VSTHRD115AvoidJoinableTaskContextCtorWithNullArgsCodeFix.SuppressWarningEquivalenceKey, + }.RunAsync(); + } + + [Fact] + public async Task ConstructorWithNonDefaultThread_SuppressAction() + { + var test = CSPreamble + """ + class Test + { + void Create2(Thread thread) => [|new JoinableTaskContext(thread)|]; + void Create1(Thread thread) => new JoinableTaskContext(thread, [|null|]); + } + """; + + var withFix = CSPreamble + """ + class Test + { + void Create2(Thread thread) => new JoinableTaskContext(thread, SynchronizationContext.Current); + void Create1(Thread thread) => new JoinableTaskContext(thread, SynchronizationContext.Current); + } + """; + + await new CSVerify.Test + { + TestCode = test, + FixedCode = withFix, + CodeActionEquivalenceKey = VSTHRD115AvoidJoinableTaskContextCtorWithNullArgsCodeFix.SuppressWarningEquivalenceKey, + }.RunAsync(); + } + + [Fact] + public async Task ConstructorWithNonDefaultThread_GetsNoFactoryAction() + { + var test = CSPreamble + """ + class Test + { + void Create2(Thread thread) => [|new JoinableTaskContext(thread)|]; + void Create1(Thread thread) => new JoinableTaskContext(thread, [|null|]); + } + """; + + await new CSVerify.Test + { + TestCode = test, + FixedCode = test, // no fix offered + CodeActionEquivalenceKey = VSTHRD115AvoidJoinableTaskContextCtorWithNullArgsCodeFix.UseFactoryMethodEquivalenceKey, + }.RunAsync(); + } + + [Fact] + public async Task ConstructorWithNullSyncContext_UseFactoryMethod() + { + var test = CSPreamble + """ + class Test + { + void Create1() => [|new JoinableTaskContext(null)|]; + void Create2() => new JoinableTaskContext(null, [|null|]); + } + """; + + var withFix = CSPreamble + """ + class Test + { + void Create1() => JoinableTaskContext.CreateNoOpContext(); + void Create2() => JoinableTaskContext.CreateNoOpContext(); + } + """; + + await new CSVerify.Test + { + TestCode = test, + FixedCode = withFix, + CodeActionEquivalenceKey = VSTHRD115AvoidJoinableTaskContextCtorWithNullArgsCodeFix.UseFactoryMethodEquivalenceKey, + }.RunAsync(); + } +} diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD200UseAsyncNamingConventionAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD200UseAsyncNamingConventionAnalyzerTests.cs index 857a9d33e..2e852c49c 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD200UseAsyncNamingConventionAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD200UseAsyncNamingConventionAnalyzerTests.cs @@ -1,24 +1,18 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Analyzers.Tests -{ - using System.Threading.Tasks; - using Microsoft.CodeAnalysis; - using Microsoft.CodeAnalysis.Testing; - using Xunit; - using Verify = CSharpCodeFixVerifier; +using CSVerify = Microsoft.VisualStudio.Threading.Analyzers.Tests.CSharpCodeFixVerifier; - public class VSTHRD200UseAsyncNamingConventionAnalyzerTests - { - private static readonly DiagnosticDescriptor AddSuffixDescriptor = VSTHRD200UseAsyncNamingConventionAnalyzer.AddAsyncDescriptor; +public class VSTHRD200UseAsyncNamingConventionAnalyzerTests +{ + private static readonly DiagnosticDescriptor AddSuffixDescriptor = VSTHRD200UseAsyncNamingConventionAnalyzer.AddAsyncDescriptor; - private static readonly DiagnosticDescriptor RemoveSuffixDescriptor = VSTHRD200UseAsyncNamingConventionAnalyzer.RemoveAsyncDescriptor; + private static readonly DiagnosticDescriptor RemoveSuffixDescriptor = VSTHRD200UseAsyncNamingConventionAnalyzer.RemoveAsyncDescriptor; - [Fact] - public async Task TaskReturningMethodWithoutSuffix_GeneratesWarning() - { - var test = @" + [Fact] + public async Task TaskReturningMethodWithoutSuffix_GeneratesWarning() + { + var test = @" using System.Threading.Tasks; class Test { @@ -26,7 +20,7 @@ class Test { } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; class Test { @@ -34,18 +28,18 @@ class Test { } "; - DiagnosticResult expected = Verify.Diagnostic(AddSuffixDescriptor).WithSpan(5, 10, 5, 13); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(AddSuffixDescriptor).WithSpan(5, 10, 5, 13); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - /// - /// Verifies that methods that return awaitable types (but without associated async method builders) - /// are allowed to include or omit the Async suffix. - /// - [Fact] - public async Task IVsTaskReturningMethod_WithGetAwaiter_GeneratesNoWarning() - { - var test = @" + /// + /// Verifies that methods that return awaitable types (but without associated async method builders) + /// are allowed to include or omit the Async suffix. + /// + [Fact] + public async Task IVsTaskReturningMethod_WithGetAwaiter_GeneratesNoWarning() + { + var test = @" using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; @@ -55,13 +49,13 @@ class Test { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task IVsTaskReturningMethodWithSuffix_NoGetAwaiter_GeneratesNoWarning() - { - var test = @" + [Fact] + public async Task IVsTaskReturningMethodWithSuffix_NoGetAwaiter_GeneratesNoWarning() + { + var test = @" using Microsoft.VisualStudio.Shell.Interop; class Test { @@ -70,13 +64,13 @@ class Test { } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task HomemadeAwaitableReturningMethodWithSuffix_GeneratesNoWarning() - { - var test = @" + [Fact] + public async Task HomemadeAwaitableReturningMethodWithSuffix_GeneratesNoWarning() + { + var test = @" using System; class Test { @@ -95,13 +89,13 @@ public void OnCompleted(Action a) { } } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task BadAwaitableReturningMethodWithSuffix_GeneratesWarning() - { - var test = @" + [Fact] + public async Task BadAwaitableReturningMethodWithSuffix_GeneratesWarning() + { + var test = @" class Test { string T() => null; string T2Async() => null; @@ -112,7 +106,7 @@ static class AwaitExtensions { } "; - var withFix = @" + var withFix = @" class Test { string T() => null; string T2() => null; @@ -123,14 +117,14 @@ static class AwaitExtensions { } "; - DiagnosticResult expected = Verify.Diagnostic(RemoveSuffixDescriptor).WithSpan(4, 12, 4, 19); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(RemoveSuffixDescriptor).WithSpan(4, 12, 4, 19); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task ValueTaskReturningMethodWithoutSuffix_GeneratesWarning() - { - var test = @" + [Fact] + public async Task ValueTaskReturningMethodWithoutSuffix_GeneratesWarning() + { + var test = @" using System.Threading.Tasks; class Test { @@ -138,7 +132,7 @@ class Test { } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; class Test { @@ -146,14 +140,14 @@ class Test { } "; - DiagnosticResult expected = Verify.Diagnostic(AddSuffixDescriptor).WithSpan(5, 15, 5, 18); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(AddSuffixDescriptor).WithSpan(5, 15, 5, 18); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task ValueTaskOfTReturningMethodWithoutSuffix_GeneratesWarning() - { - var test = @" + [Fact] + public async Task ValueTaskOfTReturningMethodWithoutSuffix_GeneratesWarning() + { + var test = @" using System.Threading.Tasks; class Test { @@ -161,7 +155,7 @@ class Test { } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; class Test { @@ -169,14 +163,14 @@ class Test { } "; - DiagnosticResult expected = Verify.Diagnostic(AddSuffixDescriptor).WithSpan(5, 20, 5, 23); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(AddSuffixDescriptor).WithSpan(5, 20, 5, 23); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task IAsyncEnumerableOfTReturningMethodWithoutSuffix_GeneratesWarning() - { - var test = @" + [Fact] + public async Task IAsyncEnumerableOfTReturningMethodWithoutSuffix_GeneratesWarning() + { + var test = @" using System.Collections.Generic; class Test { @@ -184,7 +178,7 @@ class Test { } "; - var withFix = @" + var withFix = @" using System.Collections.Generic; class Test { @@ -192,14 +186,37 @@ class Test { } "; - DiagnosticResult expected = Verify.Diagnostic(AddSuffixDescriptor).WithSpan(5, 27, 5, 30); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(AddSuffixDescriptor).WithSpan(5, 27, 5, 30); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task HomemadeIAsyncEnumerableOfTReturningMethodWithoutSuffix_GeneratesWarning() - { - var test = @" + [Fact] + public async Task IAsyncEnumeratorOfTReturningMethodWithoutSuffix_GeneratesWarning() + { + var test = @" +using System.Collections.Generic; + +class Test { + IAsyncEnumerator Foo() => default; +} +"; + + var withFix = @" +using System.Collections.Generic; + +class Test { + IAsyncEnumerator FooAsync() => default; +} +"; + + DiagnosticResult expected = CSVerify.Diagnostic(AddSuffixDescriptor).WithSpan(5, 27, 5, 30); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } + + [Fact] + public async Task HomemadeIAsyncEnumerableOfTReturningMethodWithoutSuffix_GeneratesWarning() + { + var test = @" using System.Collections.Generic; using System.Threading; @@ -212,7 +229,7 @@ class Test { } "; - var withFix = @" + var withFix = @" using System.Collections.Generic; using System.Threading; @@ -225,14 +242,14 @@ class Test { } "; - DiagnosticResult expected = Verify.Diagnostic(AddSuffixDescriptor).WithSpan(10, 28, 10, 31); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(AddSuffixDescriptor).WithSpan(10, 28, 10, 31); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task TaskReturningMainMethodWithoutSuffix_GeneratesNoWarning() - { - var test = @" + [Fact] + public async Task TaskReturningMainMethodWithoutSuffix_GeneratesNoWarning() + { + var test = @" using System.Threading.Tasks; class Test { @@ -243,20 +260,20 @@ static async Task Main() } "; - await new Verify.Test - { - TestState = - { - Sources = { test }, - OutputKind = OutputKind.ConsoleApplication, - }, - }.RunAsync(); - } - - [Fact] - public async Task TaskReturningMainMethodWithArgsWithoutSuffix_GeneratesNoWarning() + await new CSVerify.Test { - var test = @" + TestState = + { + Sources = { test }, + OutputKind = OutputKind.ConsoleApplication, + }, + }.RunAsync(); + } + + [Fact] + public async Task TaskReturningMainMethodWithArgsWithoutSuffix_GeneratesNoWarning() + { + var test = @" using System.Threading.Tasks; class Test { @@ -267,20 +284,20 @@ static async Task Main(string[] args) } "; - await new Verify.Test - { - TestState = - { - Sources = { test }, - OutputKind = OutputKind.ConsoleApplication, - }, - }.RunAsync(); - } - - [Fact] - public async Task TaskOfIntReturningMainMethodWithoutSuffix_GeneratesNoWarning() + await new CSVerify.Test { - var test = @" + TestState = + { + Sources = { test }, + OutputKind = OutputKind.ConsoleApplication, + }, + }.RunAsync(); + } + + [Fact] + public async Task TaskOfIntReturningMainMethodWithoutSuffix_GeneratesNoWarning() + { + var test = @" using System.Threading.Tasks; class Test { @@ -292,20 +309,20 @@ static async Task Main() } "; - await new Verify.Test - { - TestState = - { - Sources = { test }, - OutputKind = OutputKind.ConsoleApplication, - }, - }.RunAsync(); - } - - [Fact] - public async Task TaskReturningMethodWithoutSuffix_CodeFixUpdatesCallers() + await new CSVerify.Test { - var test = @" + TestState = + { + Sources = { test }, + OutputKind = OutputKind.ConsoleApplication, + }, + }.RunAsync(); + } + + [Fact] + public async Task TaskReturningMethodWithoutSuffix_CodeFixUpdatesCallers() + { + var test = @" using System.Threading.Tasks; class Test { @@ -317,7 +334,7 @@ async Task BarAsync() } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; class Test { @@ -329,14 +346,14 @@ async Task BarAsync() } "; - DiagnosticResult expected = Verify.Diagnostic(AddSuffixDescriptor).WithSpan(5, 10, 5, 13); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(AddSuffixDescriptor).WithSpan(5, 10, 5, 13); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task TaskReturningMethodWithoutSuffixWithMultipleOverloads_CodeFixUpdatesCallers() - { - var test = @" + [Fact] + public async Task TaskReturningMethodWithoutSuffixWithMultipleOverloads_CodeFixUpdatesCallers() + { + var test = @" using System.Threading.Tasks; class Test { @@ -350,7 +367,7 @@ async Task BarAsync() } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; class Test { @@ -364,44 +381,57 @@ async Task BarAsync() } "; - DiagnosticResult[] expected = - { - Verify.Diagnostic(AddSuffixDescriptor).WithSpan(5, 10, 5, 13), - Verify.Diagnostic(AddSuffixDescriptor).WithSpan(6, 10, 6, 13), - }; - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } - - [Fact] - public async Task TaskReturningMethodWithSuffix_GeneratesNoWarning() + DiagnosticResult[] expected = { - var test = @" + CSVerify.Diagnostic(AddSuffixDescriptor).WithSpan(5, 10, 5, 13), + CSVerify.Diagnostic(AddSuffixDescriptor).WithSpan(6, 10, 6, 13), + }; + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } + + [Fact] + public async Task TaskReturningMethodWithSuffix_GeneratesNoWarning() + { + var test = @" using System.Threading.Tasks; class Test { Task FooAsync() => null; } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task IAsyncEnumerableOfTReturningMethodWithSuffix_GeneratesNoWarning() - { - var test = @" + [Fact] + public async Task IAsyncEnumerableOfTReturningMethodWithSuffix_GeneratesNoWarning() + { + var test = @" using System.Collections.Generic; class Test { IAsyncEnumerable FooAsync() => null; } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task HomemadeIAsyncEnumerableOfTReturningMethodWithSuffix_GeneratesNoWarning() - { - var test = @" + [Fact] + public async Task IAsyncEnumeratorOfTReturningMethodWithSuffix_GeneratesNoWarning() + { + var test = @" +using System.Collections.Generic; + +class Test { + IAsyncEnumerator FooAsync() => null; +} +"; + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task HomemadeIAsyncEnumerableOfTReturningMethodWithSuffix_GeneratesNoWarning() + { + var test = @" using System.Collections.Generic; using System.Threading; @@ -413,13 +443,13 @@ class Test { MyAsyncEnumerable FooAsync() => default; } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } - [Fact] - public async Task VoidReturningMethodWithSuffix_GeneratesWarning() - { - var test = @" + [Fact] + public async Task VoidReturningMethodWithSuffix_GeneratesWarning() + { + var test = @" class Test { void FooAsync() { } @@ -427,7 +457,7 @@ void FooAsync() { } } "; - var withFix = @" + var withFix = @" class Test { void Foo() { } @@ -435,14 +465,14 @@ void Foo() { } } "; - DiagnosticResult expected = Verify.Diagnostic(RemoveSuffixDescriptor).WithSpan(3, 10, 3, 18); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(RemoveSuffixDescriptor).WithSpan(3, 10, 3, 18); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task BoolReturningMethodWithSuffix_GeneratesWarning() - { - var test = @" + [Fact] + public async Task BoolReturningMethodWithSuffix_GeneratesWarning() + { + var test = @" class Test { bool FooAsync() => false; @@ -450,7 +480,7 @@ class Test { } "; - var withFix = @" + var withFix = @" class Test { bool Foo() => false; @@ -458,14 +488,14 @@ class Test { } "; - DiagnosticResult expected = Verify.Diagnostic(RemoveSuffixDescriptor).WithSpan(3, 10, 3, 18); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(RemoveSuffixDescriptor).WithSpan(3, 10, 3, 18); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task TaskReturningMethodWithoutSuffix_ImplementsInterface_GeneratesWarningOnlyOnInterface() - { - var test = @" + [Fact] + public async Task TaskReturningMethodWithoutSuffix_ImplementsInterface_GeneratesWarningOnlyOnInterface() + { + var test = @" using System.Threading.Tasks; interface IFoo { @@ -477,7 +507,7 @@ class Test : IFoo { } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; interface IFoo { @@ -489,14 +519,14 @@ class Test : IFoo { } "; - DiagnosticResult expected = Verify.Diagnostic(AddSuffixDescriptor).WithSpan(5, 10, 5, 13); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(AddSuffixDescriptor).WithSpan(5, 10, 5, 13); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task TaskReturningMethodWithoutSuffix_ImplementsInterfaceExplicitly_GeneratesWarningOnlyOnInterface() - { - var test = @" + [Fact] + public async Task TaskReturningMethodWithoutSuffix_ImplementsInterfaceExplicitly_GeneratesWarningOnlyOnInterface() + { + var test = @" using System.Threading.Tasks; interface IFoo { @@ -508,7 +538,7 @@ class Test : IFoo { } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; interface IFoo { @@ -520,14 +550,14 @@ class Test : IFoo { } "; - DiagnosticResult expected = Verify.Diagnostic(AddSuffixDescriptor).WithSpan(5, 10, 5, 13); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(AddSuffixDescriptor).WithSpan(5, 10, 5, 13); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task TaskReturningMethodWithoutSuffix_OverridesAbstract_GeneratesWarningOnlyOnInterface() - { - var test = @" + [Fact] + public async Task TaskReturningMethodWithoutSuffix_OverridesAbstract_GeneratesWarningOnlyOnInterface() + { + var test = @" using System.Threading.Tasks; abstract class MyBase { @@ -539,7 +569,7 @@ class Test : MyBase { } "; - var withFix = @" + var withFix = @" using System.Threading.Tasks; abstract class MyBase { @@ -551,21 +581,78 @@ class Test : MyBase { } "; - DiagnosticResult expected = Verify.Diagnostic(AddSuffixDescriptor).WithSpan(5, 26, 5, 29); - await Verify.VerifyCodeFixAsync(test, expected, withFix); - } + DiagnosticResult expected = CSVerify.Diagnostic(AddSuffixDescriptor).WithSpan(5, 26, 5, 29); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } - [Fact] - public async Task TaskReturningPropertyWithoutSuffix_GeneratesNoWarning() - { - var test = @" + [Fact] + public async Task TaskReturningPropertyWithoutSuffix_GeneratesNoWarning() + { + var test = @" using System.Threading.Tasks; class Test { Task Foo => null; } "; - await Verify.VerifyAnalyzerAsync(test); - } + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task MethodDisposeAsyncCore_GeneratesNoWarning() + { + var test = @" +using System; +using System.Threading.Tasks; + +class Test : IAsyncDisposable +{ + public async ValueTask DisposeAsync() + { + await DisposeAsyncCore().ConfigureAwait(false); + GC.SuppressFinalize(this); + } + + protected virtual async ValueTask DisposeAsyncCore() + { + } +} +"; + await CSVerify.VerifyAnalyzerAsync(test); + } + + [Fact] + public async Task LocalFunctionUsesAsyncSuffix() + { + string test = """ + using System.Threading.Tasks; + + class MyClass + { + void Foo() + { + async Task {|#0:Bar|}() + { + } + } + } + """; + + string fix = """ + using System.Threading.Tasks; + + class MyClass + { + void Foo() + { + async Task BarAsync() + { + } + } + } + """; + + DiagnosticResult expected = CSVerify.Diagnostic(AddSuffixDescriptor).WithLocation(0); + await CSVerify.VerifyCodeFixAsync(test, expected, fix); } } diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/app.config b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/app.config deleted file mode 100644 index 986d387ff..000000000 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/app.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/xunit.runner.json b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/xunit.runner.json new file mode 100644 index 000000000..8465a4543 --- /dev/null +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "shadowCopy": false +} diff --git a/test/Microsoft.VisualStudio.Threading.Tests.Win7RegistryWatcher/Microsoft.VisualStudio.Threading.Tests.Win7RegistryWatcher.csproj b/test/Microsoft.VisualStudio.Threading.Tests.Win7RegistryWatcher/Microsoft.VisualStudio.Threading.Tests.Win7RegistryWatcher.csproj index 849497670..6029916ee 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests.Win7RegistryWatcher/Microsoft.VisualStudio.Threading.Tests.Win7RegistryWatcher.csproj +++ b/test/Microsoft.VisualStudio.Threading.Tests.Win7RegistryWatcher/Microsoft.VisualStudio.Threading.Tests.Win7RegistryWatcher.csproj @@ -2,6 +2,7 @@ net472 Exe + false diff --git a/test/Microsoft.VisualStudio.Threading.Tests.Win7RegistryWatcher/Program.cs b/test/Microsoft.VisualStudio.Threading.Tests.Win7RegistryWatcher/Program.cs index d950e89fe..079a3220c 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests.Win7RegistryWatcher/Program.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests.Win7RegistryWatcher/Program.cs @@ -1,19 +1,18 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.Threading.Tests.Win7RegistryWatcher -{ - using Microsoft.Win32; +using Microsoft.Win32; + +namespace Microsoft.VisualStudio.Threading.Tests.Win7RegistryWatcher; - internal static class Program +internal static class Program +{ + private static void Main() { - private static void Main() - { - // Watch a registry key. Then try to exit the program. - // If in Win7 support mode we start a thread to monitor for registry changes, - // this verifies that the thread is a *background* thread that won't keep the process running. - RegistryKey? key = Registry.CurrentUser.OpenSubKey("SOFTWARE"); - key.WaitForChangeAsync(); - } + // Watch a registry key. Then try to exit the program. + // If in Win7 support mode we start a thread to monitor for registry changes, + // this verifies that the thread is a *background* thread that won't keep the process running. + RegistryKey? key = Registry.CurrentUser.OpenSubKey("SOFTWARE"); + key.WaitForChangeAsync(); } } diff --git a/test/Microsoft.VisualStudio.Threading.Tests/.editorconfig b/test/Microsoft.VisualStudio.Threading.Tests/.editorconfig new file mode 100644 index 000000000..f13764289 --- /dev/null +++ b/test/Microsoft.VisualStudio.Threading.Tests/.editorconfig @@ -0,0 +1,5 @@ +[*.cs] +# xUnit1030: Test methods should not call ConfigureAwait(false) +dotnet_diagnostic.xUnit1030.severity = none +# xUnit1031: Test methods should not use blocking task operations +dotnet_diagnostic.xUnit1031.severity = none diff --git a/test/Microsoft.VisualStudio.Threading.Tests/App.config b/test/Microsoft.VisualStudio.Threading.Tests/App.config index 0a5a0c0f4..60b6010e6 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/App.config +++ b/test/Microsoft.VisualStudio.Threading.Tests/App.config @@ -5,8 +5,6 @@ - - - \ No newline at end of file + diff --git a/test/Microsoft.VisualStudio.Threading.Tests/AssemblyInfo.cs b/test/Microsoft.VisualStudio.Threading.Tests/AssemblyInfo.cs index 1e1b0b02e..194900e3c 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/AssemblyInfo.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/AssemblyInfo.cs @@ -1,8 +1,6 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Xunit; - // Some of our tests measure stress, GC pressure, etc. // It messes with reliable test results when other threads are doing random stuff. [assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/test/Microsoft.VisualStudio.Threading.Tests/AssertEx.cs b/test/Microsoft.VisualStudio.Threading.Tests/AssertEx.cs index 43c802cfd..279634de2 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/AssertEx.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/AssertEx.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; @@ -10,7 +10,7 @@ public static void Equal(T expected, T actual, string message) { if (!EqualityComparer.Default.Equals(expected, actual)) { - throw new Xunit.Sdk.AssertActualExpectedException(expected, actual, message); + throw Xunit.Sdk.EqualException.ForMismatchedValues(expected?.ToString() ?? string.Empty, actual?.ToString() ?? string.Empty, message); } } @@ -18,7 +18,7 @@ public static void Equal(T expected, T actual, string formattingMessage, para { if (!EqualityComparer.Default.Equals(expected, actual)) { - throw new Xunit.Sdk.AssertActualExpectedException(expected, actual, string.Format(CultureInfo.CurrentCulture, formattingMessage, formattingArgs)); + throw Xunit.Sdk.EqualException.ForMismatchedValues(expected?.ToString() ?? string.Empty, actual?.ToString() ?? string.Empty, string.Format(CultureInfo.CurrentCulture, formattingMessage, formattingArgs)); } } @@ -26,7 +26,7 @@ public static void NotEqual(T expected, T actual, string message) { if (EqualityComparer.Default.Equals(expected, actual)) { - throw new Xunit.Sdk.AssertActualExpectedException(expected, actual, message); + throw Xunit.Sdk.NotEqualException.ForEqualValues(expected?.ToString() ?? "", actual?.ToString() ?? "", message); } } @@ -34,7 +34,7 @@ public static void NotEqual(T expected, T actual, string formattingMessage, p { if (EqualityComparer.Default.Equals(expected, actual)) { - throw new Xunit.Sdk.AssertActualExpectedException(expected, actual, string.Format(CultureInfo.CurrentCulture, formattingMessage, formattingArgs)); + throw Xunit.Sdk.NotEqualException.ForEqualValues(expected?.ToString() ?? "", actual?.ToString() ?? "", string.Format(CultureInfo.CurrentCulture, formattingMessage, formattingArgs)); } } } diff --git a/test/Microsoft.VisualStudio.Threading.Tests/AsyncAutoResetEventTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/AsyncAutoResetEventTests.cs index 9e9ec8012..c6b0ecc11 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/AsyncAutoResetEventTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/AsyncAutoResetEventTests.cs @@ -1,17 +1,15 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Threading; using System.Threading.Tasks; -using Microsoft.VisualStudio.Threading; -using Xunit; public class AsyncAutoResetEventTests : TestBase { private AsyncAutoResetEvent evt; - public AsyncAutoResetEventTests(Xunit.Abstractions.ITestOutputHelper logger) + public AsyncAutoResetEventTests(ITestOutputHelper logger) : base(logger) { this.evt = new AsyncAutoResetEvent(); @@ -74,9 +72,9 @@ public void SetReturnsBeforeInlinedContinuations() .ContinueWith( delegate { - // Arrange to synchronously block the continuation until Set() has returned, - // which would deadlock if Set does not return until inlined continuations complete. - Assert.True(setReturned.Wait(AsyncDelay)); + // Arrange to synchronously block the continuation until Set() has returned, + // which would deadlock if Set does not return until inlined continuations complete. + Assert.True(setReturned.Wait(AsyncDelay)); }, TaskContinuationOptions.ExecuteSynchronously); this.evt.Set(); @@ -121,7 +119,7 @@ public void WaitAsync_WithCancellationToken_DoesNotClaimSignal() try { waitTask.GetAwaiter().GetResult(); - Assert.True(false, "Task was expected to transition to a canceled state."); + Assert.Fail("Task was expected to transition to a canceled state."); } catch (OperationCanceledException ex) { @@ -148,7 +146,7 @@ public void WaitAsync_WithCancellationToken_PrecanceledDoesNotClaimExistingSigna try { this.evt.WaitAsync(token).GetAwaiter().GetResult(); - Assert.True(false, "Task was expected to transition to a canceled state."); + Assert.Fail("Task was expected to transition to a canceled state."); } catch (OperationCanceledException ex) { @@ -200,8 +198,9 @@ public async Task WaitAsync_Canceled_Stress() /// /// Verifies that long-lived, uncanceled CancellationTokens do not result in leaking memory. /// - [SkippableFact] + [Fact(Skip = "It always fails after xunit 2.8 update.")] [Trait("GC", "true")] + [Trait("TestCategory", "FailsInCloudTest")] public async Task WaitAsync_WithCancellationToken_DoesNotLeakWhenNotCanceled() { if (await this.ExecuteInIsolationAsync()) @@ -221,8 +220,9 @@ public async Task WaitAsync_WithCancellationToken_DoesNotLeakWhenNotCanceled() /// /// Verifies that canceled CancellationTokens do not result in leaking memory. /// - [SkippableFact] + [Fact(Skip = "It always fails after xunit 2.8 update.")] [Trait("GC", "true")] + [Trait("TestCategory", "FailsInCloudTest")] public async Task WaitAsync_WithCancellationToken_DoesNotLeakWhenCanceled() { if (await this.ExecuteInIsolationAsync()) diff --git a/test/Microsoft.VisualStudio.Threading.Tests/AsyncBarrierTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/AsyncBarrierTests.cs index 9ea8854f2..394cf5bee 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/AsyncBarrierTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/AsyncBarrierTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -6,9 +6,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; public class AsyncBarrierTests : TestBase { @@ -42,6 +39,38 @@ public async Task ManyParticipantsAndSteps() await this.MultipleParticipantsHelperAsync(100, 50); } + [Fact] + public async Task SignalAndWait_PrecanceledButReady() + { + AsyncBarrier barrier = new(1); + CancellationToken precanceled = new(canceled: true); + OperationCanceledException ex = await Assert.ThrowsAnyAsync(async () => await barrier.SignalAndWait(precanceled)).WithCancellation(this.TimeoutToken); + Assert.Equal(precanceled, ex.CancellationToken); + } + + [Fact] + public async Task SignalAndWait_PrecanceledWhileWaiting() + { + AsyncBarrier barrier = new(2); + CancellationToken precanceled = new(canceled: true); + OperationCanceledException ex = await Assert.ThrowsAnyAsync(async () => await barrier.SignalAndWait(precanceled)).WithCancellation(this.TimeoutToken); + Assert.Equal(precanceled, ex.CancellationToken); + } + + [Fact] + public async Task SignalAndWait_CanceledLeavesSignalBehind() + { + AsyncBarrier barrier = new(2); + CancellationTokenSource cts = new(); + Task waiter1 = barrier.SignalAndWait(cts.Token).AsTask(); + cts.Cancel(); + OperationCanceledException ex = await Assert.ThrowsAnyAsync(() => waiter1).WithCancellation(this.TimeoutToken); + Assert.Equal(cts.Token, ex.CancellationToken); + + // Now test that the second awaiter gets in, even though the first was canceled. + await barrier.SignalAndWait().WithCancellation(this.TimeoutToken); + } + /// /// Verifies that with multiple threads constantly fulfilling the participant count /// and resetting and fulfilling it again, it still performs as expected. diff --git a/test/Microsoft.VisualStudio.Threading.Tests/AsyncCountdownEventTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/AsyncCountdownEventTests.cs index ba70fe1bb..2ab242193 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/AsyncCountdownEventTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/AsyncCountdownEventTests.cs @@ -1,12 +1,9 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Threading; using System.Threading.Tasks; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; public class AsyncCountdownEventTests : TestBase { diff --git a/test/Microsoft.VisualStudio.Threading.Tests/AsyncCrossProcessMutexTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/AsyncCrossProcessMutexTests.cs new file mode 100644 index 000000000..1223a8491 --- /dev/null +++ b/test/Microsoft.VisualStudio.Threading.Tests/AsyncCrossProcessMutexTests.cs @@ -0,0 +1,216 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Threading; +using System.Threading.Tasks; + +public class AsyncCrossProcessMutexTests : TestBase, IDisposable +{ + private readonly AsyncCrossProcessMutex mutex = new($"test {Guid.NewGuid()}"); + + public AsyncCrossProcessMutexTests(ITestOutputHelper logger) + : base(logger) + { + } + + public void Dispose() + { + this.mutex.Dispose(); + } + + [Fact] + public void DisposeWithoutUse() + { + // This test intentionally left blank. The tested functionality is in the constructor and Dispose method. + } + + [Fact] + public void Dispose_Twice() + { + // The second disposal happens in the Dispose method of this class. + this.mutex.Dispose(); + } + + [Fact] + public async Task EnterAsync_Release_Uncontested() + { + await this.VerifyMutexEnterReleaseAsync(); + } + + [Fact] + public async Task EnterAsync_DoubleRelease() + { + AsyncCrossProcessMutex.LockReleaser releaser = await this.mutex.EnterAsync(); + releaser.Dispose(); + releaser.Dispose(); + } + + [Fact] + public async Task EnterAsync_Reentrancy() + { + using (AsyncCrossProcessMutex.LockReleaser releaser = await this.mutex.EnterAsync()) + { + await Assert.ThrowsAsync(async () => await this.mutex.EnterAsync()); + } + + await this.VerifyMutexEnterReleaseAsync(); + } + + [Fact] + public async Task TryEnterAsync_Reentrancy() + { + using (AsyncCrossProcessMutex.LockReleaser releaser = await this.mutex.EnterAsync()) + { + await Assert.ThrowsAsync(async () => await this.mutex.TryEnterAsync(Timeout.InfiniteTimeSpan)); + } + + await this.VerifyMutexEnterReleaseAsync(); + } + + [Fact] + public async Task EnterAsync_Contested() + { + // We don't allow attempted reentrancy, so create a new mutex object to use for the contested locks. + using AsyncCrossProcessMutex mutex2 = new(this.mutex.Name); + + // Acquire and hold the mutex so that we can test timeout behavior. + using (AsyncCrossProcessMutex.LockReleaser releaser = await this.mutex.EnterAsync()) + { + // Verify that we can't acquire the mutex within a timeout. + await Assert.ThrowsAsync(async () => await mutex2.EnterAsync(TimeSpan.Zero)); + await Assert.ThrowsAsync(async () => await mutex2.EnterAsync(TimeSpan.FromMilliseconds(1))); + } + + // Verify that we can acquire the mutex after it is released. + using (AsyncCrossProcessMutex.LockReleaser releaser2 = await mutex2.EnterAsync()) + { + } + + // Verify that the main mutex still functions. + await this.VerifyMutexEnterReleaseAsync(); + } + + [Fact] + public async Task TryEnterAsync_Contested() + { + // We don't allow attempted reentrancy, so create a new mutex object to use for the contested locks. + using AsyncCrossProcessMutex mutex2 = new(this.mutex.Name); + + // Acquire and hold the mutex so that we can test timeout behavior. + using (AsyncCrossProcessMutex.LockReleaser? releaser = await this.mutex.TryEnterAsync(Timeout.InfiniteTimeSpan)) + { + // Verify that we can't acquire the mutex within a timeout. + Assert.Null(await mutex2.TryEnterAsync(TimeSpan.Zero)); + Assert.Null(await mutex2.TryEnterAsync(TimeSpan.FromMilliseconds(1))); + + // Just verify that the syntax is nice. + using (AsyncCrossProcessMutex.LockReleaser? releaser2 = await mutex2.TryEnterAsync(TimeSpan.Zero)) + { + Assert.Null(releaser2); + } + } + + // Verify that we can acquire the mutex after it is released. + using (AsyncCrossProcessMutex.LockReleaser releaser2 = await mutex2.EnterAsync()) + { + } + + // Verify that the main mutex still functions. + await this.VerifyMutexEnterReleaseAsync(); + } + + [Fact] + public async Task EnterAsync_InvalidNegativeTimeout() + { + await Assert.ThrowsAsync(async () => await this.mutex.EnterAsync(TimeSpan.FromMilliseconds(-2))); + await this.VerifyMutexEnterReleaseAsync(); + } + + [Fact] + public async Task TryEnterAsync_InvalidNegativeTimeout() + { + await Assert.ThrowsAsync(async () => await this.mutex.TryEnterAsync(TimeSpan.FromMilliseconds(-2))); + await this.VerifyMutexEnterReleaseAsync(); + } + + [Fact] + public async Task EnterAsync_AbandonedMutex() + { + using AsyncCrossProcessMutex mutex2 = new(this.mutex.Name); + + AsyncCrossProcessMutex.LockReleaser abandonedReleaser = await this.mutex.EnterAsync(); + Assert.False(abandonedReleaser.IsAbandoned); + + // Dispose the mutex WITHOUT first releasing it. + this.mutex.Dispose(); + + using (AsyncCrossProcessMutex.LockReleaser releaser2 = await mutex2.EnterAsync()) + { + Assert.True(releaser2.IsAbandoned); + } + } + + [Fact] + public async Task TryEnterAsync_AbandonedMutex() + { + using AsyncCrossProcessMutex mutex2 = new(this.mutex.Name); + + AsyncCrossProcessMutex.LockReleaser? abandonedReleaser = await this.mutex.TryEnterAsync(Timeout.InfiniteTimeSpan); + Assert.False(abandonedReleaser?.IsAbandoned); + + // Dispose the mutex WITHOUT first releasing it. + this.mutex.Dispose(); + + using (AsyncCrossProcessMutex.LockReleaser? releaser2 = await mutex2.TryEnterAsync(Timeout.InfiniteTimeSpan)) + { + Assert.True(releaser2?.IsAbandoned); + } + } + + [Fact] + public async Task EnterAsync_ThrowsObjectDisposedException() + { + this.mutex.Dispose(); + await Assert.ThrowsAsync(async () => await this.mutex.EnterAsync()); + } + + [Fact] + public async Task TryEnterAsync_ThrowsObjectDisposedException() + { + this.mutex.Dispose(); + await Assert.ThrowsAsync(async () => await this.mutex.TryEnterAsync(Timeout.InfiniteTimeSpan)); + } + + [Fact] + public async Task TryEnterAsync_Twice() + { + using (AsyncCrossProcessMutex.LockReleaser? releaser = await this.mutex.TryEnterAsync(TimeSpan.Zero)) + { + } + + using (AsyncCrossProcessMutex.LockReleaser? releaser = await this.mutex.TryEnterAsync(TimeSpan.Zero)) + { + } + } + + /// + /// Asserts behavior or the .NET Mutex class that we may be emulating in our class. + /// + [Fact] + public void Mutex_BaselineBehaviors() + { + // Verify reentrant behavior. + using Mutex mutex = new(false, $"test {Guid.NewGuid()}"); + mutex.WaitOne(); + mutex.WaitOne(); + mutex.ReleaseMutex(); + mutex.ReleaseMutex(); + Assert.Throws(mutex.ReleaseMutex); + } + + private async Task VerifyMutexEnterReleaseAsync() + { + using AsyncCrossProcessMutex.LockReleaser releaser = await this.mutex.EnterAsync(); + } +} diff --git a/test/Microsoft.VisualStudio.Threading.Tests/AsyncLazyInitializerTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/AsyncLazyInitializerTests.cs index c07ac405e..69eb2dccd 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/AsyncLazyInitializerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/AsyncLazyInitializerTests.cs @@ -1,12 +1,9 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Threading; using System.Threading.Tasks; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; public class AsyncLazyInitializerTests : TestBase { diff --git a/test/Microsoft.VisualStudio.Threading.Tests/AsyncLazyTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/AsyncLazyTests.cs index 9ed35c929..051c960d5 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/AsyncLazyTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/AsyncLazyTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -6,9 +6,8 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; + +using Microsoft; using NamedSyncContext = AwaitExtensionsTests.NamedSyncContext; public class AsyncLazyTests : TestBase @@ -18,6 +17,13 @@ public AsyncLazyTests(ITestOutputHelper logger) { } + public enum DisposeStyle + { + IDisposable, + SystemIAsyncDisposable, + ThreadingIAsyncDisposable, + } + [Fact] public async Task Basic() { @@ -147,10 +153,11 @@ public async Task ValueFactoryReleasedAfterExecution() { WeakReference collectible = await this.ValueFactoryReleasedAfterExecution_Helper(); + await Task.Yield(); for (int i = 0; i < 3; i++) { - await Task.Yield(); - GC.Collect(); + GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, blocking: true); + GC.WaitForPendingFinalizers(); } Assert.False(collectible.IsAlive); @@ -161,10 +168,11 @@ public async Task AsyncPumpReleasedAfterExecution(bool throwInValueFactory) { WeakReference collectible = await this.AsyncPumpReleasedAfterExecution_Helper(throwInValueFactory); + await Task.Yield(); for (int i = 0; i < 3; i++) { - await Task.Yield(); - GC.Collect(); + GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, blocking: true); + GC.WaitForPendingFinalizers(); } Assert.False(collectible.IsAlive); @@ -203,7 +211,7 @@ public async Task ValueFactoryReentersValueFactorySynchronously(bool specifyJtf) Assert.False(executed); executed = true; lazy!.GetValueAsync(); - return Task.FromResult(new object()); + return Task.FromResult(new object()); }, jtf); @@ -378,8 +386,8 @@ public void ToStringForUncreatedValue() [Fact] public async Task ToStringForCreatedValue() { - var lazy = new AsyncLazy(() => Task.FromResult(3)); - var value = await lazy.GetValueAsync(); + var lazy = new AsyncLazy(() => Task.FromResult(3)); + int value = await lazy.GetValueAsync(); string result = lazy.ToString(); Assert.Equal(value.ToString(CultureInfo.InvariantCulture), result); } @@ -474,7 +482,7 @@ async delegate // the Main thread waiting for it to complete. // This will deadlock unless the AsyncLazy joins // the value factory's async pump with the currently blocking one. - var value = await lazy.GetValueAsync(); + object value = await lazy.GetValueAsync(); Assert.NotNull(value); }); @@ -512,7 +520,7 @@ async delegate }, passJtfToLazyCtor ? asyncPump : null); // mix it up to exercise all the code paths in the ctor. - var backgroundRequest = Task.Run(async delegate + Task backgroundRequest = Task.Run(async delegate { return await lazy.GetValueAsync(); }); @@ -521,7 +529,7 @@ async delegate Task? foregroundRequest = lazy.GetValueAsync(); SingleThreadedTestSynchronizationContext.IFrame? frame = SingleThreadedTestSynchronizationContext.NewFrame(); - var combinedTask = Task.WhenAll(foregroundRequest, backgroundRequest); + Task combinedTask = Task.WhenAll(foregroundRequest, backgroundRequest); combinedTask.WithTimeout(UnexpectedTimeout).ContinueWith(_ => frame.Continue = false, TaskScheduler.Default); SingleThreadedTestSynchronizationContext.PushFrame(ctxt, frame); @@ -560,7 +568,7 @@ async delegate }, asyncPump); - var backgroundRequest = Task.Run(async delegate + Task backgroundRequest = Task.Run(async delegate { return await lazy.GetValueAsync(); }); @@ -568,8 +576,8 @@ async delegate Thread.Sleep(AsyncDelay); // Give the background thread time to call GetValueAsync(), but it doesn't yield (when the test was written). asyncPump.Run(async delegate { - var foregroundValue = await lazy.GetValueAsync(this.TimeoutToken); - var backgroundValue = await backgroundRequest; + object foregroundValue = await lazy.GetValueAsync(this.TimeoutToken); + object backgroundValue = await backgroundRequest; Assert.Same(foregroundValue, backgroundValue); }); } @@ -603,14 +611,14 @@ async delegate }, asyncPump); - var backgroundRequest = Task.Run(async delegate + Task backgroundRequest = Task.Run(async delegate { return await lazy.GetValueAsync(); }); Thread.Sleep(AsyncDelay); // Give the background thread time to call GetValueAsync(), but it doesn't yield (when the test was written). - var foregroundValue = lazy.GetValue(this.TimeoutToken); - var backgroundValue = asyncPump.Run(() => backgroundRequest); + object foregroundValue = lazy.GetValue(this.TimeoutToken); + object backgroundValue = asyncPump.Run(() => backgroundRequest); Assert.Same(foregroundValue, backgroundValue); } @@ -644,6 +652,364 @@ public async Task ExecutionContextFlowsFromFirstCaller_JTF() await asyncLazy.GetValueAsync(); } + [Fact] + public async Task SuppressRelevance_WithoutJTF() + { + AsyncManualResetEvent allowValueFactoryToFinish = new(); + Task? fireAndForgetTask = null; + AsyncLazy asyncLazy = null!; + asyncLazy = new AsyncLazy( + async delegate + { + using (asyncLazy.SuppressRelevance()) + { + fireAndForgetTask = FireAndForgetCodeAsync(); + } + + await allowValueFactoryToFinish; + return 1; + }, + null); + + bool fireAndForgetCodeAsyncEntered = false; + Task lazyValue = asyncLazy.GetValueAsync(); + Assert.True(fireAndForgetCodeAsyncEntered); + allowValueFactoryToFinish.Set(); + + // Assert that the value factory was allowed to finish. + Assert.Equal(1, await lazyValue.WithCancellation(this.TimeoutToken)); + + // Assert that the fire-and-forget task was allowed to finish and did so without throwing. + Assert.Equal(1, await fireAndForgetTask!.WithCancellation(this.TimeoutToken)); + + async Task FireAndForgetCodeAsync() + { + fireAndForgetCodeAsyncEntered = true; + return await asyncLazy.GetValueAsync(); + } + } + + [Fact] + public async Task SuppressRelevance_WithJTF() + { + JoinableTaskContext? context = this.InitializeJTCAndSC(); + SingleThreadedTestSynchronizationContext.IFrame frame = SingleThreadedTestSynchronizationContext.NewFrame(); + + JoinableTaskFactory? jtf = context.Factory; + AsyncManualResetEvent allowValueFactoryToFinish = new(); + Task? fireAndForgetTask = null; + AsyncLazy asyncLazy = null!; + asyncLazy = new AsyncLazy( + async delegate + { + using (asyncLazy.SuppressRelevance()) + { + fireAndForgetTask = FireAndForgetCodeAsync(); + } + + await allowValueFactoryToFinish; + return 1; + }, + jtf); + + bool fireAndForgetCodeAsyncEntered = false; + bool fireAndForgetCodeAsyncReachedUIThread = false; + jtf.Run(async delegate + { + Task lazyValue = asyncLazy.GetValueAsync(); + Assert.True(fireAndForgetCodeAsyncEntered); + await Task.Delay(AsyncDelay); + Assert.False(fireAndForgetCodeAsyncReachedUIThread); + allowValueFactoryToFinish.Set(); + + // Assert that the value factory was allowed to finish. + Assert.Equal(1, await lazyValue.WithCancellation(this.TimeoutToken)); + }); + + // Run a main thread pump so the fire-and-forget task can finish. + SingleThreadedTestSynchronizationContext.PushFrame(SynchronizationContext.Current!, frame); + + // Assert that the fire-and-forget task was allowed to finish and did so without throwing. + Assert.Equal(1, await fireAndForgetTask!.WithCancellation(this.TimeoutToken)); + + async Task FireAndForgetCodeAsync() + { + fireAndForgetCodeAsyncEntered = true; + + // Yield the caller's thread. + // Resuming will require the main thread, since the caller was on the main thread. + await Task.Yield(); + + fireAndForgetCodeAsyncReachedUIThread = true; + + int result = await asyncLazy.GetValueAsync(); + frame.Continue = false; + return result; + } + } + + [Fact] + public async Task SuppressRecursiveFactoryDetection_WithoutJTF() + { + AsyncManualResetEvent allowValueFactoryToFinish = new(); + Task? fireAndForgetTask = null; + AsyncLazy asyncLazy = null!; + asyncLazy = new AsyncLazy( + async delegate + { + fireAndForgetTask = FireAndForgetCodeAsync(); + await allowValueFactoryToFinish; + return 1; + }, + null) + { + SuppressRecursiveFactoryDetection = true, + }; + + bool fireAndForgetCodeAsyncEntered = false; + Task lazyValue = asyncLazy.GetValueAsync(); + Assert.True(fireAndForgetCodeAsyncEntered); + allowValueFactoryToFinish.Set(); + + // Assert that the value factory was allowed to finish. + Assert.Equal(1, await lazyValue.WithCancellation(this.TimeoutToken)); + + // Assert that the fire-and-forget task was allowed to finish and did so without throwing. + Assert.Equal(1, await fireAndForgetTask!.WithCancellation(this.TimeoutToken)); + + async Task FireAndForgetCodeAsync() + { + fireAndForgetCodeAsyncEntered = true; + return await asyncLazy.GetValueAsync(); + } + } + + [Theory, PairwiseData] + public async Task SuppressRecursiveFactoryDetection_WithJTF(bool suppressWithJTF) + { + JoinableTaskContext? context = this.InitializeJTCAndSC(); + SingleThreadedTestSynchronizationContext.IFrame frame = SingleThreadedTestSynchronizationContext.NewFrame(); + + JoinableTaskFactory? jtf = context.Factory; + AsyncManualResetEvent allowValueFactoryToFinish = new(); + Task? fireAndForgetTask = null; + AsyncLazy asyncLazy = null!; + asyncLazy = new AsyncLazy( + async delegate + { + using (suppressWithJTF ? jtf.Context.SuppressRelevance() : default) + using (suppressWithJTF ? default : asyncLazy.SuppressRelevance()) + { + fireAndForgetTask = FireAndForgetCodeAsync(); + } + + await allowValueFactoryToFinish; + return 1; + }, + jtf) + { + SuppressRecursiveFactoryDetection = true, + }; + + bool fireAndForgetCodeAsyncEntered = false; + bool fireAndForgetCodeAsyncReachedUIThread = false; + jtf.Run(async delegate + { + Task lazyValue = asyncLazy.GetValueAsync(); + Assert.True(fireAndForgetCodeAsyncEntered); + await Task.Delay(AsyncDelay); + Assert.False(fireAndForgetCodeAsyncReachedUIThread); + allowValueFactoryToFinish.Set(); + + // Assert that the value factory was allowed to finish. + Assert.Equal(1, await lazyValue.WithCancellation(this.TimeoutToken)); + }); + + // Run a main thread pump so the fire-and-forget task can finish. + SingleThreadedTestSynchronizationContext.PushFrame(SynchronizationContext.Current!, frame); + + // Assert that the fire-and-forget task was allowed to finish and did so without throwing. + Assert.Equal(1, await fireAndForgetTask!.WithCancellation(this.TimeoutToken)); + + async Task FireAndForgetCodeAsync() + { + fireAndForgetCodeAsyncEntered = true; + + // Yield the caller's thread. + // Resuming will require the main thread, since the caller was on the main thread. + await Task.Yield(); + + fireAndForgetCodeAsyncReachedUIThread = true; + + int result = await asyncLazy.GetValueAsync(); + frame.Continue = false; + return result; + } + } + + [Fact] + public async Task Dispose_ValueType_Completed() + { + AsyncLazy lazy = new(() => Task.FromResult(3)); + lazy.GetValue(); + lazy.DisposeValue(); + await this.AssertDisposedLazyAsync(lazy); + } + + [Theory, CombinatorialData] + public async Task Dispose_Disposable_Completed(DisposeStyle variety) + { + AsyncLazy lazy = new(() => Task.FromResult(DisposableFactory(variety))); + DisposableBase value = (DisposableBase)lazy.GetValue(); + lazy.DisposeValue(); + Assert.True(value.IsDisposed); + await this.AssertDisposedLazyAsync(lazy); + } + + [Fact] + public async Task Dispose_NonDisposable_Completed() + { + AsyncLazy lazy = new(() => Task.FromResult(new object())); + lazy.GetValue(); + lazy.DisposeValue(); + await this.AssertDisposedLazyAsync(lazy); + } + + [Theory, CombinatorialData] + public async Task Dispose_Disposable_Incomplete(DisposeStyle variety) + { + AsyncManualResetEvent unblock = new(); + AsyncLazy lazy = new(async delegate + { + await unblock; + return DisposableFactory(variety); + }); + Task lazyTask = lazy.GetValueAsync(this.TimeoutToken); + Task disposeTask = lazy.DisposeValueAsync(); + await Assert.ThrowsAnyAsync(() => disposeTask.WithTimeout(ExpectedTimeout)); + unblock.Set(); + await disposeTask.WithCancellation(this.TimeoutToken); + DisposableBase value = (DisposableBase)await lazyTask; + await this.AssertDisposedLazyAsync(lazy); + await value.Disposed.WithCancellation(this.TimeoutToken); + } + + [Fact] + public void DisposeValue_AsyncDisposableValueRequiresMainThread() + { + JoinableTaskContext context = this.InitializeJTCAndSC(); + SingleThreadedTestSynchronizationContext.IFrame frame = SingleThreadedTestSynchronizationContext.NewFrame(); + + AsyncLazy lazy = new( + delegate + { + return Task.FromResult(new SystemAsyncDisposable { YieldDuringDispose = true }); + }, + context.Factory); + lazy.GetValue(); + + TaskCompletionSource delegateResult = new(); + SynchronizationContext.Current!.Post( + delegate + { + try + { + lazy.DisposeValue(); + + delegateResult.SetResult(true); + } + catch (Exception ex) + { + delegateResult.SetException(ex); + } + finally + { + frame.Continue = false; + } + }, + null); + SingleThreadedTestSynchronizationContext.PushFrame(SynchronizationContext.Current!, frame); + delegateResult.Task.GetAwaiter().GetResult(); // rethrow any exceptions + } + + [Fact] + public async Task Dispose_NonDisposable_Incomplete() + { + AsyncManualResetEvent unblock = new(); + AsyncLazy lazy = new(async delegate + { + await unblock; + return new object(); + }); + Task lazyTask = lazy.GetValueAsync(this.TimeoutToken); + Task disposeTask = lazy.DisposeValueAsync(); + await Assert.ThrowsAnyAsync(() => disposeTask.WithTimeout(ExpectedTimeout)); + unblock.Set(); + await disposeTask.WithCancellation(this.TimeoutToken); + await lazyTask; + await this.AssertDisposedLazyAsync(lazy); + } + + [Fact] + public async Task Dispose_CalledTwice_NotStarted() + { + bool valueFactoryExecuted = false; + AsyncLazy lazy = new(() => + { + valueFactoryExecuted = true; + return Task.FromResult(new object()); + }); + lazy.DisposeValue(); + lazy.DisposeValue(); + await this.AssertDisposedLazyAsync(lazy); + Assert.False(valueFactoryExecuted); + } + + [Fact] + public async Task Dispose_CalledTwice_NonDisposable_Completed() + { + AsyncLazy lazy = new(() => Task.FromResult(new object())); + lazy.GetValue(); + lazy.DisposeValue(); + lazy.DisposeValue(); + await this.AssertDisposedLazyAsync(lazy); + } + + [Fact] + public async Task Dispose_CalledTwice_Disposable_Completed() + { + AsyncLazy lazy = new(() => Task.FromResult(new Disposable())); + lazy.GetValue(); + lazy.DisposeValue(); + lazy.DisposeValue(); + await this.AssertDisposedLazyAsync(lazy); + } + + [Fact] + public void DisposeValue_MidFactoryThatContestsForMainThread() + { + JoinableTaskContext context = this.InitializeJTCAndSC(); + + AsyncLazy lazy = new( + async delegate + { + // Ensure the caller keeps control of the UI thread, + // so that the request for the main thread comes in when it's controlled by others. + await Task.Yield(); + await context.Factory.SwitchToMainThreadAsync(this.TimeoutToken); + return new(); + }, + context.Factory); + + Task lazyFactory = lazy.GetValueAsync(this.TimeoutToken); + + // Make a JTF blocking call on the main thread that won't return until the factory completes. + context.Factory.Run(async delegate + { + await lazy.DisposeValueAsync().WithCancellation(this.TimeoutToken); + }); + } + [Fact(Skip = "Hangs. This test documents a deadlock scenario that is not fixed (by design, IIRC).")] public async Task ValueFactoryRequiresReadLockHeldByOther() { @@ -689,6 +1055,14 @@ async delegate } } + private static DisposableBase DisposableFactory(DisposeStyle variety) => variety switch + { + DisposeStyle.IDisposable => new Disposable(), + DisposeStyle.SystemIAsyncDisposable => new SystemAsyncDisposable(), + DisposeStyle.ThreadingIAsyncDisposable => new ThreadingAsyncDisposable(), + _ => throw new NotSupportedException(), + }; + private JoinableTaskContext InitializeJTCAndSC() { SynchronizationContext.SetSynchronizationContext(SingleThreadedTestSynchronizationContext.New()); @@ -727,4 +1101,49 @@ private async Task AsyncPumpReleasedAfterExecution_Helper(bool th await lazy.GetValueAsync().NoThrowAwaitable(); return collectible; } + + private async Task AssertDisposedLazyAsync(AsyncLazy lazy) + { + Assert.False(lazy.IsValueCreated); + Assert.False(lazy.IsValueFactoryCompleted); + Assert.Throws(() => lazy.GetValue()); + await Assert.ThrowsAsync(lazy.GetValueAsync); + } + + private abstract class DisposableBase + { + protected readonly AsyncManualResetEvent disposalEvent = new(); + + public Task Disposed => this.disposalEvent.WaitAsync(); + + public bool IsDisposed => this.disposalEvent.IsSet; + } + + private class Disposable : DisposableBase, IDisposableObservable + { + public void Dispose() => this.disposalEvent.Set(); + } + + private class SystemAsyncDisposable : DisposableBase, System.IAsyncDisposable + { + internal bool YieldDuringDispose { get; set; } + + public async ValueTask DisposeAsync() + { + this.disposalEvent.Set(); + if (this.YieldDuringDispose) + { + await Task.Yield(); + } + } + } + + private class ThreadingAsyncDisposable : DisposableBase, Microsoft.VisualStudio.Threading.IAsyncDisposable + { + public Task DisposeAsync() + { + this.disposalEvent.Set(); + return Task.CompletedTask; + } + } } diff --git a/test/Microsoft.VisualStudio.Threading.Tests/AsyncLocalTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/AsyncLocalTests.cs index 9045a9ded..0d0b63e88 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/AsyncLocalTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/AsyncLocalTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -7,9 +7,6 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; public class AsyncLocalTests : TestBase { diff --git a/test/Microsoft.VisualStudio.Threading.Tests/AsyncManualResetEventTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/AsyncManualResetEventTests.cs index 00315e306..beaf94e5b 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/AsyncManualResetEventTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/AsyncManualResetEventTests.cs @@ -1,12 +1,9 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Threading; using System.Threading.Tasks; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; public class AsyncManualResetEventTests : TestBase { @@ -21,7 +18,7 @@ public AsyncManualResetEventTests(ITestOutputHelper logger) [Fact] public void CtorDefaultParameter() { - Assert.False(new System.Threading.ManualResetEventSlim().IsSet); + Assert.False(new ManualResetEventSlim().IsSet); } [Fact] @@ -62,6 +59,87 @@ public void SetReturnsBeforeInlinedContinuations() Assert.True(inlinedContinuation.Wait(UnexpectedTimeout)); } + /// + /// Verifies that inlining continuations do not delay the Task returned from SetAsync() from completing. + /// + [Fact] + public void SetAsyncReturnsCompletedTaskBeforeNonInlinedContinuationsComplete() + { + ManualResetEventSlim setReturned = new(); + Task? inlinedContinuation = this.evt.WaitAsync() + .ContinueWith( + delegate + { + // Arrange to synchronously block the continuation until released, + // which would deadlock if SetAsync's Task does not complete until inlined continuations complete. + Assert.True(setReturned.Wait(UnexpectedTimeout)); + }, + TaskContinuationOptions.ExecuteSynchronously); +#pragma warning disable CS0618 // Type or member is obsolete + Task setTask = this.evt.SetAsync(); +#pragma warning restore CS0618 // Type or member is obsolete + Assert.True(setTask.IsCompleted); + Assert.True(this.evt.IsSet); + + // Release the continuation so the test doesn't leak a thread. + setReturned.Set(); + + Assert.True(inlinedContinuation.Wait(UnexpectedTimeout)); + } + + /// + /// Verifies that inlining continuations do not delay the Task returned from SetAsync() from completing. + /// + [Fact] + public void SetAsyncAndWaitAsyncReturnsCompletedTaskBeforeInlinedContinuationsComplete() + { + // Reconfigure the object to allow inlining awaiters. + this.evt = new(allowInliningAwaiters: true); + + // Arrange for an awaiter that is not only inlined, but synchronously blocks + // its caller, such that the SetAsync method that completes the Task cannot immediately return. + ManualResetEventSlim continuationStarted = new(); + ManualResetEventSlim setReturned = new(); + Task? inlinedContinuation = this.evt.WaitAsync() + .ContinueWith( + delegate + { + continuationStarted.Set(); + + // Arrange to synchronously block the continuation until released, + // which would deadlock if SetAsync's Task does not complete until inlined continuations complete. + Assert.True(setReturned.Wait(UnexpectedTimeout)); + }, + TaskContinuationOptions.ExecuteSynchronously); + +#pragma warning disable CS0618 // Type or member is obsolete + // First, spin off a thread to call SetAsync. This will be blocked. + Task.Run(() => this.evt.SetAsync(), TestContext.Current.CancellationToken); + + // Now wait for the continuation to start, to verify that SetAsync has done its work, + // even if it can't return to us. + Assert.True(continuationStarted.Wait(UnexpectedTimeout)); + + // Verify that the event is set, even though SetAsync has not yet returned. + Assert.True(this.evt.IsSet); + + // Now call SetAsync again. This time it should return immediately + // even though the first call hasn't yet, and it should return a completed Task. + Task setTask = this.evt.SetAsync(); +#pragma warning restore CS0618 // Type or member is obsolete + + Assert.True(setTask.IsCompleted); + + // Also verify that WaitAsync returns a completed Task, even though the first SetAsync call hasn't yet returned. + Assert.True(this.evt.WaitAsync().IsCompleted); + + // Release the continuation so the test doesn't leak a thread. + setReturned.Set(); + + // Verify that the inlined continuation has completed and didn't throw. + Assert.True(inlinedContinuation.Wait(UnexpectedTimeout)); + } + [Fact] public async Task Blocking() { diff --git a/test/Microsoft.VisualStudio.Threading.Tests/AsyncQueueTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/AsyncQueueTests.cs index a039a09eb..c53a34c2c 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/AsyncQueueTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/AsyncQueueTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -6,14 +6,12 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.VisualStudio.Threading; -using Xunit; public class AsyncQueueTests : TestBase { private AsyncQueue queue; - public AsyncQueueTests(Xunit.Abstractions.ITestOutputHelper logger) + public AsyncQueueTests(ITestOutputHelper logger) : base(logger) { this.queue = new AsyncQueue(); @@ -25,6 +23,7 @@ public void JustInitialized() Assert.Equal(0, this.queue.Count); Assert.True(this.queue.IsEmpty); Assert.False(this.queue.Completion.IsCompleted); + Assert.Empty(this.queue.ToArray()); } [Fact] @@ -34,6 +33,7 @@ public void Enqueue() this.queue.Enqueue(value); Assert.Equal(1, this.queue.Count); Assert.False(this.queue.IsEmpty); + Assert.Single(this.queue.ToArray()); } [Fact] @@ -43,6 +43,7 @@ public void TryEnqueue() Assert.True(this.queue.TryEnqueue(value)); Assert.Equal(1, this.queue.Count); Assert.False(this.queue.IsEmpty); + Assert.Single(this.queue.ToArray()); } [Fact] @@ -540,7 +541,8 @@ public void OnCompletedInvoked() Assert.Equal(1, invoked); } - [SkippableFact, Trait("GC", "true")] + [Fact, Trait("GC", "true")] + [Trait("TestCategory", "FailsInCloudTest")] public void UnusedQueueGCPressure() { if (this.ExecuteInIsolation()) diff --git a/test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterLockTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterLockTests.cs index e5b6d5368..e3daf893c 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterLockTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterLockTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -9,9 +9,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; using Xunit.Sdk; #pragma warning disable CA1416 // Validate platform compatibility @@ -230,7 +227,8 @@ public async Task CompleteMethodExecutesContinuationsAsynchronously() await continuation; } - [SkippableFact] + [Fact] + [Trait("TestCategory", "FailsInCloudTest")] public async Task NoMemoryLeakForManyLocks() { if (await this.ExecuteInIsolationAsync()) @@ -2652,7 +2650,7 @@ await Task.Run(delegate try { awaiter.GetResult(); - Assert.True(false, "Expected OperationCanceledException not thrown."); + Assert.Fail("Expected OperationCanceledException not thrown."); } catch (OperationCanceledException) { @@ -2670,7 +2668,7 @@ public void PrecancelledWriteLockAsyncRequestOnSTA() try { awaiter.GetResult(); - Assert.True(false, "Expected OperationCanceledException not thrown."); + Assert.Fail("Expected OperationCanceledException not thrown."); } catch (OperationCanceledException) { @@ -2702,7 +2700,7 @@ await Task.WhenAll( try { awaiter.GetResult(); - cancellationTestConcluded.SetException(new ThrowsException(typeof(OperationCanceledException))); + cancellationTestConcluded.SetException(ThrowsException.ForNoException(typeof(OperationCanceledException))); } catch (OperationCanceledException) { @@ -2740,7 +2738,7 @@ await Task.WhenAll( try { awaiter.GetResult(); - cancellationTestConcluded.SetException(new ThrowsException(typeof(OperationCanceledException))); + cancellationTestConcluded.SetException(ThrowsException.ForNoException(typeof(OperationCanceledException))); } catch (OperationCanceledException) { @@ -3026,7 +3024,7 @@ public void CompleteBlocksNewTopLevelLocksSTA() try { awaiter.GetResult(); - Assert.True(false, "Expected exception not thrown."); + Assert.Fail("Expected exception not thrown."); } catch (InvalidOperationException) { @@ -3047,7 +3045,7 @@ await Task.Run(delegate try { awaiter.GetResult(); - Assert.True(false, "Expected exception not thrown."); + Assert.Fail("Expected exception not thrown."); } catch (InvalidOperationException) { @@ -3448,7 +3446,7 @@ public async Task OnBeforeWriteLockReleasedDelegateThrows() this.asyncLock.Complete(); } - Assert.True(false, "Expected exception not thrown."); + Assert.Fail("Expected exception not thrown."); } catch (AggregateException ex) { @@ -3853,10 +3851,10 @@ public async Task MtaLockSharedWithMta() } /// Verifies that when an MTA holding a lock traverses (via CallContext) to an STA that the STA does not appear to hold a lock. - [SkippableFact] + [Fact] public async Task MtaLockNotSharedWithSta() { - Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Windows only"); using (await this.asyncLock.ReadLockAsync()) { var testComplete = new TaskCompletionSource(); @@ -3879,10 +3877,10 @@ public async Task MtaLockNotSharedWithSta() } /// Verifies that when an MTA holding a lock traverses (via CallContext) to an STA that the STA will be able to access the same lock by marshaling back to an MTA. - [SkippableFact] + [Fact] public async Task ReadLockTraversesAcrossSta() { - Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Windows only"); using (await this.asyncLock.ReadLockAsync()) { var testComplete = new TaskCompletionSource(); @@ -3919,10 +3917,10 @@ public async Task ReadLockTraversesAcrossSta() } /// Verifies that when an MTA holding a lock traverses (via CallContext) to an STA that the STA will be able to access the same lock by requesting it and moving back to an MTA. - [SkippableFact] + [Fact] public async Task UpgradeableReadLockTraversesAcrossSta() { - Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Windows only"); using (await this.asyncLock.UpgradeableReadLockAsync()) { @@ -3962,10 +3960,10 @@ public async Task UpgradeableReadLockTraversesAcrossSta() } /// Verifies that when an MTA holding a lock traverses (via CallContext) to an STA that the STA will be able to access the same lock by requesting it and moving back to an MTA. - [SkippableFact] + [Fact] public async Task WriteLockTraversesAcrossSta() { - Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Windows only"); using (await this.asyncLock.WriteLockAsync()) { var testComplete = new TaskCompletionSource(); @@ -4086,6 +4084,70 @@ await Assert.ThrowsAsync(async delegate } } + [Fact] + public async Task UpgradeableReadLockForksAndAsksForReadLock() + { + var lck = new LockWithForkDetection(); + using (await lck.UpgradeableReadLockAsync()) + { + await Task.Run(async delegate + { + // This requests a read lock from a thread pool thread (no NonConcurrentSynchronizationContext) + // while holding an upgradeable read lock. This is a fork of the exclusive context and should + // be detected via the OnLockForkDetected virtual method. + using (await lck.ReadLockAsync()) + { + Assert.True(lck.IsReadLockHeld); + } + }); + } + + Assert.Equal(1, lck.ForkDetectedCount); + lck.Complete(); + Assert.True(lck.Completion.Wait(UnexpectedTimeout)); + } + + [Fact] + public async Task UpgradeableReadLockDoesNotForkOnCorrectContext() + { + var lck = new LockWithForkDetection(); + using (await lck.UpgradeableReadLockAsync()) + { + // Request a nested read lock on the correct context (with NonConcurrentSynchronizationContext). + // This should NOT trigger fork detection. + using (await lck.ReadLockAsync()) + { + Assert.True(lck.IsReadLockHeld); + } + } + + Assert.Equal(0, lck.ForkDetectedCount); + lck.Complete(); + Assert.True(lck.Completion.Wait(UnexpectedTimeout)); + } + + [Fact] + public async Task ReadLockForkFromReadOnlyContextDoesNotTriggerDetection() + { + var lck = new LockWithForkDetection(); + using (await lck.ReadLockAsync()) + { + await Task.Run(async delegate + { + // Fork from a read-only context. This should NOT trigger fork detection + // because read locks don't require NonConcurrentSynchronizationContext. + using (await lck.ReadLockAsync()) + { + Assert.True(lck.IsReadLockHeld); + } + }); + } + + Assert.Equal(0, lck.ForkDetectedCount); + lck.Complete(); + Assert.True(lck.Completion.Wait(UnexpectedTimeout)); + } + [Fact] public async Task WriteNestsReadWithWriteReleasedFirst() { @@ -4184,7 +4246,7 @@ public async Task WriteNestsReadWithWriteReleasedFirstWithoutTaskRun() try { await completingTask; // observe any exception. - Assert.True(false, "Expected exception not thrown."); + Assert.Fail("Expected exception not thrown."); } catch (CriticalErrorException ex) { @@ -4227,7 +4289,7 @@ public async Task SetLockDataNoLock() lck.SetLockData(null); Assert.Null(lck.GetLockData()); - var value1 = new object(); + object value1 = new object(); lck.SetLockData(value1); Assert.Equal(value1, lck.GetLockData()); @@ -4235,7 +4297,7 @@ public async Task SetLockDataNoLock() { Assert.Null(lck.GetLockData()); - var value2 = new object(); + object value2 = new object(); lck.SetLockData(value2); Assert.Equal(value2, lck.GetLockData()); } @@ -4348,106 +4410,114 @@ public async Task ReadLockAsync_Await_CapturesExecutionContext() [Fact] public async Task ReadLockAsync_OnCompleted_CapturesExecutionContext() { - var asyncLocal = new Microsoft.VisualStudio.Threading.AsyncLocal(); - asyncLocal.Value = "expected"; - AsyncReaderWriterLock.Awaiter? awaiter = this.asyncLock.ReadLockAsync().GetAwaiter(); - Assumes.False(awaiter.IsCompleted); - var testResultSource = new TaskCompletionSource(); - awaiter.OnCompleted(delegate + // Set a lock-incompatible synchronization context to ensure the issued lock will require us to yield. + using (new SynchronizationContext().Apply(checkForChangesOnRevert: false)) { - try + var asyncLocal = new Microsoft.VisualStudio.Threading.AsyncLocal(); + asyncLocal.Value = "expected"; + AsyncReaderWriterLock.Awaiter? awaiter = this.asyncLock.ReadLockAsync().GetAwaiter(); + Assumes.False(awaiter.IsCompleted); + var testResultSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + awaiter.OnCompleted(delegate { - using (awaiter.GetResult()) + try { - Assert.Equal("expected", asyncLocal.Value); - testResultSource.SetResult(null); + using (awaiter.GetResult()) + { + Assert.Equal("expected", asyncLocal.Value); + testResultSource.SetResult(null); + } } - } - catch (Exception ex) - { - testResultSource.SetException(ex); - } - finally - { - } - }); - await testResultSource.Task; + catch (Exception ex) + { + testResultSource.SetException(ex); + } + finally + { + } + }); + await testResultSource.Task; + } } [Fact] public async Task ReadLockAsync_UnsafeOnCompleted_DoesNotCaptureExecutionContext() { - var asyncLocal = new Microsoft.VisualStudio.Threading.AsyncLocal(); - asyncLocal.Value = "expected"; - AsyncReaderWriterLock.Awaiter? awaiter = this.asyncLock.ReadLockAsync().GetAwaiter(); - Assumes.False(awaiter.IsCompleted); - var testResultSource = new TaskCompletionSource(); - awaiter.UnsafeOnCompleted(delegate + // Apply some SynchronizationContext to ensure the issued lock will require us to yield. + using (new SynchronizationContext().Apply(checkForChangesOnRevert: false)) { - try + var asyncLocal = new Microsoft.VisualStudio.Threading.AsyncLocal(); + asyncLocal.Value = "expected"; + AsyncReaderWriterLock.Awaiter? awaiter = this.asyncLock.ReadLockAsync().GetAwaiter(); + Assumes.False(awaiter.IsCompleted); + TaskCompletionSource testResultSource = new(TaskCreationOptions.RunContinuationsAsynchronously); + awaiter.UnsafeOnCompleted(delegate { - using (awaiter.GetResult()) + try { - Assert.Null(asyncLocal.Value); - testResultSource.SetResult(null); + using (awaiter.GetResult()) + { + Assert.Null(asyncLocal.Value); + testResultSource.SetResult(null); + } } - } - catch (Exception ex) - { - testResultSource.SetException(ex); - } - finally - { - } - }); - await testResultSource.Task; + catch (Exception ex) + { + testResultSource.SetException(ex); + } + finally + { + } + }); + await testResultSource.Task; + } } - [Fact] + [Fact(Skip = "Disabled after dependency updates introduced failures. See https://dev.azure.com/devdiv/DevDiv/_workitems/edit/1974921")] public async Task ReadLockAsync_UseTaskScheduler() { var asyncLock = new AsyncReaderWriterLockWithSpecialScheduler(); - Assumes.Equals(0, asyncLock.StartedTaskCount); + Assert.Equal(0, asyncLock.StartedTaskCount); // A reader lock issued immediately will not be rescheduled. using (await asyncLock.ReadLockAsync()) { } - Assumes.Equals(0, asyncLock.StartedTaskCount); + Assert.Equal(0, asyncLock.StartedTaskCount); var writeLockObtained = new AsyncManualResetEvent(); var readLockObtained = new AsyncManualResetEvent(); var writeLockToRelease = new AsyncManualResetEvent(); var writeLockTask = Task.Run(async () => { - using (await asyncLock.WriteLockAsync()) + using (await asyncLock.WriteLockAsync(this.TimeoutToken)) { // Write lock is not scheduled through the read lock scheduler. - Assumes.Equals(0, asyncLock.StartedTaskCount); + Assert.Equal(0, asyncLock.StartedTaskCount); writeLockObtained.Set(); - await writeLockToRelease.WaitAsync(); + await writeLockToRelease.WaitAsync(this.TimeoutToken); } }); - await writeLockObtained.WaitAsync(); + await writeLockObtained.WaitAsync(this.TimeoutToken); var readLockTask = Task.Run(async () => { - using (await asyncLock.ReadLockAsync()) + using (await asyncLock.ReadLockAsync(this.TimeoutToken)) { // Newly issued read lock is using the task scheduler. - Assumes.Equals(1, asyncLock.StartedTaskCount); + Assert.Equal(1, asyncLock.StartedTaskCount); // Unstable test. Sometimes it's 1, sometimes it's 2. readLockObtained.Set(); } }); - await asyncLock.ScheduleSemaphore.WaitAsync(); + await asyncLock.ScheduleSemaphore.WaitAsync(this.TimeoutToken); writeLockToRelease.Set(); - await writeLockTask; - await readLockTask; + await writeLockTask.WithCancellation(this.TimeoutToken); + await readLockTask.WithCancellation(this.TimeoutToken); - Assumes.Equals(1, asyncLock.StartedTaskCount); + Assert.Equal(1, asyncLock.StartedTaskCount); // Unstable test. Sometimes it's 1, sometimes it's 2. } [Fact] @@ -4512,7 +4582,7 @@ await Task.WhenAll( await secondLockInQueue.SetAsync(); }), secondLockObtained.Task); - }); + }); } private Task UncontestedTopLevelLocksAllocFreeHelperAsync(Func locker, bool yieldingLock) @@ -4993,7 +5063,7 @@ private async Task MitigationAgainstAccidentalLockForkingHelper(Func this.forkDetectedCount; + + protected override void OnLockForkDetected() + { + Interlocked.Increment(ref this.forkDetectedCount); + } + } } diff --git a/test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterResourceLockTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterResourceLockTests.cs index d02e2b66e..da1e01462 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterResourceLockTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/AsyncReaderWriterResourceLockTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -7,9 +7,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; public class AsyncReaderWriterResourceLockTests : TestBase { @@ -310,7 +307,7 @@ public async Task PreparationSucceedsForConcurrentReadersWhenOneCancels() try { await resourceTask; - Assert.True(false, "Expected OperationCanceledException not thrown."); + Assert.Fail("Expected OperationCanceledException not thrown."); } catch (OperationCanceledException) { @@ -461,7 +458,7 @@ public async Task PreparationIsAppliedToResourceImpactedByOutsideChangePredicate (resource, s) => { Assert.Same(state, s); - Assert.True(false, "Read locks should not invoke this."); + Assert.Fail("Read locks should not invoke this."); return false; }, state); @@ -762,6 +759,31 @@ public async Task PreparationReservesLock() Assert.True(!preparationStartTask.Result.CanBeCanceled); } + [Fact] + public async Task LockServiceMayPreservesThrottlingScheduler() + { + var resourceTask = new TaskCompletionSource(); + + this.resourceLock.SetPreparationCallback( + this.resources[1], + (r, c) => + { + resourceTask.SetResult(TaskScheduler.Current); + return Task.CompletedTask; + }); + + var schedulerPair = new ConcurrentExclusiveSchedulerPair(TaskScheduler.Default, 2); + + await schedulerPair.ConcurrentScheduler.SwitchTo(); + + using (AsyncReaderWriterResourceLock.ResourceReleaser access = await this.resourceLock.ReadLockAsync()) + { + _ = await access.GetResourceAsync(1); + } + + Assert.Equal(schedulerPair.ConcurrentScheduler, await resourceTask.Task); + } + [Fact] public async Task PreparationResourceTaskCanBeCancelled() { @@ -1191,7 +1213,7 @@ public async Task GetResourceAsyncRetriesFaultedPreparation() try { await access.GetResourceAsync(1); - Assert.True(false, "Expected exception not thrown."); + Assert.Fail("Expected exception not thrown."); } catch (ApplicationException) { @@ -1217,7 +1239,7 @@ public async Task PrepareResourceForConcurrentAccessAsync_ThrowsDuringReadShould try { Resource? resource = await access.GetResourceAsync(1); - Assert.True(false, "Expected exception not thrown."); + Assert.Fail("Expected exception not thrown."); } catch (ApplicationException) { @@ -1250,7 +1272,7 @@ public async Task PrepareResourceForConcurrentAccessAsync_ThrowsReleasingWriteSh try { await writeAccess.ReleaseAsync(); - Assert.True(false, "Expected exception not thrown."); + Assert.Fail("Expected exception not thrown."); } catch (ApplicationException) { @@ -1260,7 +1282,7 @@ public async Task PrepareResourceForConcurrentAccessAsync_ThrowsReleasingWriteSh } // Exiting the using block should also throw. - Assert.True(false, "Expected exception not thrown."); + Assert.Fail("Expected exception not thrown."); } catch (ApplicationException) { @@ -1283,7 +1305,7 @@ public async Task PrepareResourceForConcurrentAccessAsync_ThrowsReleasingWriteSh try { await readAccess.GetResourceAsync(1); - Assert.True(false, "Expected exception not thrown."); + Assert.Fail("Expected exception not thrown."); } catch (ApplicationException) { @@ -1519,7 +1541,7 @@ private class ResourceLockWrapper : AsyncReaderWriterResourceLock { private readonly List resources; - private readonly Dictionary, Task>> preparationTasks = new Dictionary, Task>>(); + private readonly Dictionary preparationTasks = new Dictionary(); private readonly AsyncAutoResetEvent preparationTaskBegun = new AsyncAutoResetEvent(); @@ -1538,6 +1560,11 @@ internal ResourceLockWrapper(List resources, ITestOutputHelper logger, this.logger = logger; } + private interface IResourcePreparation + { + Task PrepareResourceAsync(Resource resource, CancellationToken cancellationToken); + } + internal AsyncAutoResetEvent PreparationTaskBegun { get { return this.preparationTaskBegun; } @@ -1553,12 +1580,20 @@ internal Task SetPreparationTask(Resource resource, Task task var tcs = new TaskCompletionSource(); lock (this.preparationTasks) { - this.preparationTasks[resource] = Tuple.Create(tcs, task); + this.preparationTasks[resource] = new ResourcePreparationState(tcs, task); } return tcs.Task; } + internal void SetPreparationCallback(Resource resource, Func callback) + { + lock (this.preparationTasks) + { + this.preparationTasks[resource] = new ResourcePreparationCallback(callback); + } + } + internal new void SetResourceAsAccessed(Resource resource) { base.SetResourceAsAccessed(resource); @@ -1579,6 +1614,11 @@ protected override Task GetResourceAsync(int resourceMoniker, Cancella return Task.FromResult(this.resources[resourceMoniker]); } + protected override TaskScheduler GetTaskSchedulerToPrepareResourcesForConcurrentAccess(Resource resource) + { + return TaskScheduler.Current.MaximumConcurrencyLevel > 1 ? TaskScheduler.Current : TaskScheduler.Default; + } + protected override async Task PrepareResourceForConcurrentAccessAsync(Resource resource, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -1615,22 +1655,54 @@ private async Task GetPreparationTask(Resource resource, CancellationToken cance Assert.True(this.IsWriteLockHeld || !this.IsAnyLockHeld); Assert.False(Monitor.IsEntered(this.SyncObject)); - Tuple, Task>? tuple; + IResourcePreparation? resourcePreparation; lock (this.preparationTasks) { - if (this.preparationTasks.TryGetValue(resource, out tuple)) + if (this.preparationTasks.TryGetValue(resource, out resourcePreparation)) { this.preparationTasks.Remove(resource); // consume task } } - if (tuple is object) + if (resourcePreparation is object) { - tuple.Item1.SetResult(cancellationToken); // signal that the preparation method has been entered - await tuple.Item2; + await resourcePreparation.PrepareResourceAsync(resource, cancellationToken); } Assert.True(this.IsWriteLockHeld || !this.IsAnyLockHeld); } + + private class ResourcePreparationState : IResourcePreparation + { + private readonly TaskCompletionSource preparationStart; + private readonly Task preparationTask; + + public ResourcePreparationState(TaskCompletionSource preparationStart, Task preparationTask) + { + this.preparationStart = preparationStart; + this.preparationTask = preparationTask; + } + + public Task PrepareResourceAsync(Resource resource, CancellationToken cancellationToken) + { + this.preparationStart.SetResult(cancellationToken); // signal that the preparation method has been entered + return this.preparationTask; + } + } + + private class ResourcePreparationCallback : IResourcePreparation + { + private readonly Func callback; + + public ResourcePreparationCallback(Func callback) + { + this.callback = callback; + } + + public Task PrepareResourceAsync(Resource resource, CancellationToken cancellationToken) + { + return this.callback(resource, cancellationToken); + } + } } } diff --git a/test/Microsoft.VisualStudio.Threading.Tests/AsyncSemaphoreTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/AsyncSemaphoreTests.cs index 440094445..7a5ffd2b1 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/AsyncSemaphoreTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/AsyncSemaphoreTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -7,9 +7,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; public class AsyncSemaphoreTests : TestBase { @@ -78,7 +75,7 @@ public async Task Contested_OneWaitsAtATime(int initialCount) // The second wave cannot enter the semaphore until space is available. for (int i = 0; i < initialCount; i++) { - var nextContestedIndex = initialCount + i; + int nextContestedIndex = initialCount + i; releasers[nextContestedIndex] = this.lck.EnterAsync(); Assert.False(releasers[nextContestedIndex].IsCompleted); releasers[i].Result.Dispose(); // exit the semaphore with a previously assigned one. @@ -108,7 +105,7 @@ public async Task Contested_ManyWaitAtATime(int initialCount) // The second wave cannot enter the semaphore until space is available. for (int i = 0; i < initialCount; i++) { - var nextContestedIndex = initialCount + i; + int nextContestedIndex = initialCount + i; releasers[nextContestedIndex] = this.lck.EnterAsync(); Assert.False(releasers[nextContestedIndex].IsCompleted); } @@ -158,7 +155,7 @@ public async Task ContestedAndCancelledWithTimeoutSpecified() try { await second; - Assert.True(false, "Expected OperationCanceledException not thrown."); + Assert.Fail("Expected OperationCanceledException not thrown."); } catch (OperationCanceledException ex) { @@ -176,7 +173,7 @@ public void PreCancelled() try { enterAsyncTask.GetAwaiter().GetResult(); - Assert.True(false, "Expected exception not thrown."); + Assert.Fail("Expected exception not thrown."); } catch (OperationCanceledException ex) { diff --git a/test/Microsoft.VisualStudio.Threading.Tests/AwaitExtensionsTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/AwaitExtensionsTests.cs index c293bc189..a120ec547 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/AwaitExtensionsTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/AwaitExtensionsTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -9,10 +9,7 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; -using Microsoft.VisualStudio.Threading; using Microsoft.Win32; -using Xunit; -using Xunit.Abstractions; #pragma warning disable CA1416 // Validate platform compatibility @@ -383,10 +380,10 @@ public async Task WaitForExit_NullArgument() await Assert.ThrowsAsync(() => AwaitExtensions.WaitForExitAsync(null!)); } - [SkippableFact] + [Fact] public async Task WaitForExitAsync_ExitCode() { - Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Windows only"); Process p = Process.Start( new ProcessStartInfo("cmd.exe", "/c exit /b 55") { @@ -397,10 +394,10 @@ public async Task WaitForExitAsync_ExitCode() Assert.Equal(55, exitCode); } - [SkippableFact] + [Fact] public void WaitForExitAsync_AlreadyExited() { - Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Windows only"); Process p = Process.Start( new ProcessStartInfo("cmd.exe", "/c exit /b 55") { @@ -507,7 +504,7 @@ public async Task ConfigureAwaitForAggregateException_ThrowsAggregateException() try { await joint.ConfigureAwaitForAggregateException(); - Assert.False(true, "Exception was not thrown."); + Assert.Fail("Exception was not thrown."); } catch (AggregateException ex) { @@ -523,7 +520,7 @@ public async Task ConfigureAwaitForAggregateException_Canceled() try { await canceled.ConfigureAwaitForAggregateException(); - Assert.False(true, "Exception was not thrown."); + Assert.Fail("Exception was not thrown."); } catch (OperationCanceledException) { @@ -540,7 +537,7 @@ public async Task ConfigureAwaitForAggregateException_InnerCanceled() try { await joint.ConfigureAwaitForAggregateException(); - Assert.False(true, "Exception was not thrown."); + Assert.Fail("Exception was not thrown."); } catch (OperationCanceledException) { @@ -557,7 +554,7 @@ public async Task ConfigureAwaitForAggregateException_InnerCanceledAndFaulted() try { await joint.ConfigureAwaitForAggregateException(); - Assert.False(true, "Exception was not thrown."); + Assert.Fail("Exception was not thrown."); } catch (AggregateException ex) { @@ -566,10 +563,57 @@ public async Task ConfigureAwaitForAggregateException_InnerCanceledAndFaulted() } } - [SkippableFact] + [Fact] + public void GetAwaiter_SynchronizationContext_ValidatesArgs() + { + Assert.Throws(() => AwaitExtensions.GetAwaiter((SynchronizationContext)null!)); + } + + [Fact] + public async Task SyncContext_Awaiter() + { + TaskCompletionSource syncContextSource = new(TaskCreationOptions.RunContinuationsAsynchronously); + SingleThreadedSynchronizationContext.Frame frame = new(); + Thread? otherThread = null; + Task otherThreadTask = Task.Run(delegate + { + SingleThreadedSynchronizationContext syncContext; + try + { + syncContext = new(); + otherThread = Thread.CurrentThread; + syncContextSource.SetResult(syncContext); + } + catch (Exception ex) + { + syncContextSource.SetException(ex); + throw; + } + + syncContext.PushFrame(frame); + }); + + try + { + SynchronizationContext context = await syncContextSource.Task; + Assert.NotSame(Thread.CurrentThread, otherThread); + await context; + Assert.Same(Thread.CurrentThread, otherThread); + await Task.Yield(); + Assert.Same(Thread.CurrentThread, otherThread); + await TaskScheduler.Default.SwitchTo(alwaysYield: true); + Assert.NotSame(Thread.CurrentThread, otherThread); + } + finally + { + frame.Continue = false; + } + } + + [Fact] public async Task AwaitRegKeyChange() { - Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Windows only"); using (var test = new RegKeyTest()) { Task changeWatcherTask = test.Key.WaitForChangeAsync(); @@ -579,10 +623,10 @@ public async Task AwaitRegKeyChange() } } - [SkippableFact] + [Fact] public async Task AwaitRegKeyChange_TwoAtOnce_SameKeyHandle() { - Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Windows only"); using (var test = new RegKeyTest()) { Task changeWatcherTask1 = test.Key.WaitForChangeAsync(); @@ -594,10 +638,10 @@ public async Task AwaitRegKeyChange_TwoAtOnce_SameKeyHandle() } } - [SkippableFact] + [Fact] public async Task AwaitRegKeyChange_NoChange() { - Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Windows only"); using (var test = new RegKeyTest()) { Task changeWatcherTask = test.Key.WaitForChangeAsync(cancellationToken: test.FinishedToken); @@ -609,10 +653,10 @@ public async Task AwaitRegKeyChange_NoChange() } } - [SkippableFact] + [Fact] public async Task AwaitRegKeyChange_WatchSubtree() { - Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Windows only"); using (var test = new RegKeyTest()) { using (RegistryKey? subKey = test.CreateSubKey()) @@ -624,10 +668,10 @@ public async Task AwaitRegKeyChange_WatchSubtree() } } - [SkippableFact] + [Fact] public async Task AwaitRegKeyChange_KeyDeleted() { - Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Windows only"); using (var test = new RegKeyTest()) { using (RegistryKey? subKey = test.CreateSubKey()) @@ -639,10 +683,10 @@ public async Task AwaitRegKeyChange_KeyDeleted() } } - [SkippableFact] + [Fact] public async Task AwaitRegKeyChange_NoWatchSubtree() { - Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Windows only"); using (var test = new RegKeyTest()) { using (RegistryKey? subKey = test.CreateSubKey()) @@ -658,10 +702,10 @@ public async Task AwaitRegKeyChange_NoWatchSubtree() } } - [SkippableFact] + [Fact] public async Task AwaitRegKeyChange_Canceled() { - Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Windows only"); using (var test = new RegKeyTest()) { var cts = new CancellationTokenSource(); @@ -671,7 +715,7 @@ public async Task AwaitRegKeyChange_Canceled() try { await changeWatcherTask; - Assert.True(false, "Expected exception not thrown."); + Assert.Fail("Expected exception not thrown."); } catch (OperationCanceledException ex) { @@ -680,10 +724,10 @@ public async Task AwaitRegKeyChange_Canceled() } } - [SkippableFact] + [Fact] public async Task AwaitRegKeyChange_KeyDisposedWhileWatching() { - Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Windows only"); Task watchingTask; using (var test = new RegKeyTest()) { @@ -694,10 +738,10 @@ public async Task AwaitRegKeyChange_KeyDisposedWhileWatching() await watchingTask; } - [SkippableFact] + [Fact] public async Task AwaitRegKeyChange_CanceledAndImmediatelyDisposed() { - Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Windows only"); Task watchingTask; CancellationToken expectedCancellationToken; using (var test = new RegKeyTest()) @@ -709,7 +753,7 @@ public async Task AwaitRegKeyChange_CanceledAndImmediatelyDisposed() try { await watchingTask; - Assert.True(false, "Expected exception not thrown."); + Assert.Fail("Expected exception not thrown."); } catch (OperationCanceledException ex) { @@ -717,10 +761,10 @@ public async Task AwaitRegKeyChange_CanceledAndImmediatelyDisposed() } } - [SkippableFact] + [Fact] public async Task AwaitRegKeyChange_CallingThreadDestroyed() { - Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Windows only"); using (var test = new RegKeyTest()) { // Start watching and be certain the thread that started watching is destroyed. @@ -742,11 +786,11 @@ public async Task AwaitRegKeyChange_CallingThreadDestroyed() } } - [SkippableFact] + [Fact] public async Task AwaitRegKeyChange_DoesNotPreventAppTerminationOnWin7() { - Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); - string testExePath = Path.Combine( + Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Windows only"); + string testExePath = Path.GetFullPath(Path.Combine( AppDomain.CurrentDomain.BaseDirectory!, "..", "..", @@ -758,7 +802,7 @@ public async Task AwaitRegKeyChange_DoesNotPreventAppTerminationOnWin7() "Release", #endif "net472", - "Microsoft.VisualStudio.Threading.Tests.Win7RegistryWatcher.exe"); + "Microsoft.VisualStudio.Threading.Tests.Win7RegistryWatcher.exe")); this.Logger.WriteLine("Using testexe path: {0}", testExePath); var psi = new ProcessStartInfo(testExePath) { diff --git a/test/Microsoft.VisualStudio.Threading.Tests/CancellationTokenExtensionsTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/CancellationTokenExtensionsTests.cs index 50912cd80..2d483b9ec 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/CancellationTokenExtensionsTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/CancellationTokenExtensionsTests.cs @@ -1,11 +1,9 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Linq; using System.Threading; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; public class CancellationTokenExtensionsTests : TestBase { @@ -226,19 +224,23 @@ public void CombineWith_Array_TwoCancelable_AmidMany(bool cancelFirst) } } - [Fact] - public void CombineWith_Array_ThreeCancelable_AmidMany() + [Theory, CombinatorialData] + public void CombineWith_Array_ThreeCancelable_AmidMany([CombinatorialRange(0, 3)] int canceledIndex) { - var cts1 = new CancellationTokenSource(); - var cts2 = new CancellationTokenSource(); - var cts3 = new CancellationTokenSource(); - using (CancellationTokenExtensions.CombinedCancellationToken combined = CancellationToken.None.CombineWith(cts1.Token, CancellationToken.None, cts2.Token, CancellationToken.None, cts3.Token)) + CancellationTokenSource[] cts = new CancellationTokenSource[3]; + for (int i = 0; i < 3; i++) { - Assert.NotEqual(cts1.Token, combined.Token); - Assert.NotEqual(cts2.Token, combined.Token); - Assert.NotEqual(cts3.Token, combined.Token); + cts[i] = new(); + } - cts2.Cancel(); + using (CancellationTokenExtensions.CombinedCancellationToken combined = cts[0].Token.CombineWith(cts.Skip(1).Select(s => s.Token).ToArray())) + { + for (int i = 0; i < cts.Length; i++) + { + Assert.NotEqual(cts[i].Token, combined.Token); + } + + cts[canceledIndex].Cancel(); Assert.True(combined.Token.IsCancellationRequested); } } diff --git a/test/Microsoft.VisualStudio.Threading.Tests/CoWaitMainThreadTransition.cs b/test/Microsoft.VisualStudio.Threading.Tests/CoWaitMainThreadTransition.cs new file mode 100644 index 000000000..f10cc753d --- /dev/null +++ b/test/Microsoft.VisualStudio.Threading.Tests/CoWaitMainThreadTransition.cs @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if NETFRAMEWORK + +using System; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; +using System.Threading; + +/// +/// Probes whether the calling STA thread's current synchronous wait allows COM RPC calls to +/// penetrate. Spawns an MTA thread that marshals a COM call back to the STA thread; the call +/// succeeds if and only if the thread is performing a CoWait (message-pumping wait) rather than +/// a plain WaitForMultipleObjects wait. +/// +/// +/// Create the probe before blocking the STA thread, then call after unblocking +/// to learn whether the COM call was delivered. Dispose when done to cancel any pending call and +/// release resources. +/// +public sealed class CoWaitMainThreadTransition : IDisposable +{ + /// Best-effort delay in milliseconds used when cancelling or joining the caller thread. + private const int CallCancellationDelayMs = 500; + + private const int RpcECallCanceled = unchecked((int)0x80010002); + + private static readonly Guid IDispatchGuid = new("00020400-0000-0000-C000-000000000046"); + + private readonly ManualResetEventSlim signalReceived = new(); + private readonly ManualResetEventSlim callerReady = new(); + private readonly Thread callerThread; + private Exception? backgroundFailure; + private uint callerThreadId; + + /// + /// Initializes a new instance of the class and + /// immediately starts the background MTA thread that will attempt the COM call. + /// + internal CoWaitMainThreadTransition() + { + IMainThreadSignaler signaler = new MainThreadSignaler(this.signalReceived); + IntPtr signalerInterface = Marshal.GetIDispatchForObject(signaler); + try + { + Marshal.ThrowExceptionForHR(NativeMethods.CoMarshalInterThreadInterfaceInStream(in IDispatchGuid, signalerInterface, out IntPtr stream)); + + this.callerThread = new Thread(() => this.InvokeSignalOnBackgroundThread(stream)) + { + IsBackground = true, + }; + } + finally + { + Marshal.Release(signalerInterface); + } + +#pragma warning disable CA1416 // Apartment state is only relevant on Windows, and the probe is not used elsewhere. + this.callerThread.SetApartmentState(ApartmentState.MTA); +#pragma warning restore CA1416 + this.callerThread.Start(); + } + + /// + /// A COM-visible IDispatch interface used to signal the main STA thread from an MTA background thread. + /// + [ComVisible(true)] + [Guid("A1D1F0E7-564F-4B9F-8DB2-D40185F115FB")] + [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] + public interface IMainThreadSignaler + { + /// Signals the main thread that it has received a COM RPC call. + [DispId(1)] + void Signal(); + } + + /// + public void Dispose() + { + if (this.callerThread.IsAlive) + { + this.CancelPendingCall(); + _ = this.callerThread.Join(CallCancellationDelayMs); + } + + this.callerReady.Dispose(); + this.signalReceived.Dispose(); + } + + /// + /// Blocks until the COM call completes or elapses. + /// + /// The maximum time to wait. + /// + /// if the COM call was delivered within ; + /// if the call did not penetrate the wait (i.e., no CoWait was used). + /// + internal bool Wait(TimeSpan timeout) + { + bool interruptedWait = this.signalReceived.Wait(timeout); + if (!interruptedWait) + { + this.CancelPendingCall(); + } + + if (interruptedWait) + { + Assert.True(this.callerThread.Join(timeout), "Timed out waiting for the COM call to finish."); + } + else + { + _ = this.callerThread.Join(CallCancellationDelayMs); + } + + if (this.backgroundFailure is object && (interruptedWait || this.backgroundFailure.HResult != RpcECallCanceled)) + { + ExceptionDispatchInfo.Capture(this.backgroundFailure).Throw(); + } + + return interruptedWait; + } + + private void CancelPendingCall() + { + Assert.True(this.callerReady.Wait(CallCancellationDelayMs), "Timed out waiting for the COM caller thread to initialize."); + if (this.callerThread.IsAlive && this.callerThreadId != 0) + { + _ = NativeMethods.CoCancelCall(this.callerThreadId, 0); + } + } + + private void InvokeSignalOnBackgroundThread(IntPtr stream) + { + try + { + this.callerThreadId = NativeMethods.GetCurrentThreadId(); + Marshal.ThrowExceptionForHR(NativeMethods.CoEnableCallCancellation(IntPtr.Zero)); + this.callerReady.Set(); + + Thread.Sleep(50); + Marshal.ThrowExceptionForHR(NativeMethods.CoGetInterfaceAndReleaseStream(stream, in IDispatchGuid, out object signaler)); + signaler.GetType().InvokeMember(nameof(IMainThreadSignaler.Signal), BindingFlags.InvokeMethod, binder: null, target: signaler, args: Array.Empty()); + } + catch (Exception ex) + { + this.backgroundFailure = ex; + this.callerReady.Set(); + } + finally + { + _ = NativeMethods.CoDisableCallCancellation(IntPtr.Zero); + } + } + + /// + /// COM-visible implementation of that uses the free-threaded + /// marshaler so the COM proxy routes calls back to whichever STA thread holds the object. + /// + [ComVisible(true)] + [ClassInterface(ClassInterfaceType.None)] + public sealed class MainThreadSignaler : StandardOleMarshalObject, IMainThreadSignaler + { + private readonly ManualResetEventSlim signalReceived; + + /// Initializes a new instance of the class. + /// The event to set when is called. + internal MainThreadSignaler(ManualResetEventSlim signalReceived) + { + this.signalReceived = signalReceived; + } + + /// + public void Signal() => this.signalReceived.Set(); + } + + private static class NativeMethods + { + [DllImport("ole32.dll")] + internal static extern int CoMarshalInterThreadInterfaceInStream(in Guid riid, IntPtr pUnk, out IntPtr ppStm); + + [DllImport("ole32.dll")] + internal static extern int CoGetInterfaceAndReleaseStream(IntPtr pStm, in Guid iid, [MarshalAs(UnmanagedType.IDispatch)] out object ppv); + + [DllImport("ole32.dll")] + internal static extern int CoEnableCallCancellation(IntPtr pReserved); + + [DllImport("ole32.dll")] + internal static extern int CoDisableCallCancellation(IntPtr pReserved); + + [DllImport("ole32.dll")] + internal static extern int CoCancelCall(uint dwThreadId, uint ulTimeout); + + [DllImport("kernel32.dll")] + internal static extern uint GetCurrentThreadId(); + } +} +#endif diff --git a/test/Microsoft.VisualStudio.Threading.Tests/DelegatingJoinableTaskFactoryTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/DelegatingJoinableTaskFactoryTests.cs index 6122888a1..2904d364d 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/DelegatingJoinableTaskFactoryTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/DelegatingJoinableTaskFactoryTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -7,9 +7,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; public class DelegatingJoinableTaskFactoryTests : JoinableTaskTestBase { @@ -156,7 +153,7 @@ protected override void WaitSynchronously(Task task) base.WaitSynchronously(task); } - protected override void PostToUnderlyingSynchronizationContext(System.Threading.SendOrPostCallback callback, object state) + protected override void PostToUnderlyingSynchronizationContext(SendOrPostCallback callback, object state) { this.addToLog(FactoryLogEntry.InnerPostToUnderlyingSynchronizationContext); base.PostToUnderlyingSynchronizationContext(callback, state); @@ -196,7 +193,7 @@ protected override void WaitSynchronously(Task task) base.WaitSynchronously(task); } - protected override void PostToUnderlyingSynchronizationContext(System.Threading.SendOrPostCallback callback, object state) + protected override void PostToUnderlyingSynchronizationContext(SendOrPostCallback callback, object state) { this.addToLog(FactoryLogEntry.OuterPostToUnderlyingSynchronizationContext); base.PostToUnderlyingSynchronizationContext(callback, state); diff --git a/test/Microsoft.VisualStudio.Threading.Tests/DispatcherExtensionsTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/DispatcherExtensionsTests.cs index f40d1d6f3..44b5cf011 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/DispatcherExtensionsTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/DispatcherExtensionsTests.cs @@ -1,7 +1,7 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -#if NETFRAMEWORK +#if NETFRAMEWORK || WINDOWS using System; using System.Threading; @@ -9,8 +9,6 @@ using System.Windows.Threading; using Microsoft; using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; public class DispatcherExtensionsTests : JoinableTaskTestBase { @@ -68,6 +66,50 @@ public void WithPriority_LowPriorityCanBlockOnHighPriorityWork() await Task.WhenAll(idleTask.Task, normalTask.Task).WithCancellation(this.TimeoutToken); }); } + +#if NETFRAMEWORK + [StaFact] + public void WithPriority_MatchesDisableProcessingWithinDelegate() + { + this.SimulateUIThread(delegate + { + JoinableTaskFactory? normalPriorityJtf = this.asyncPump.WithPriority(Dispatcher.CurrentDispatcher, DispatcherPriority.Normal); + normalPriorityJtf.Run(delegate + { + this.AssertProcessingAllowed(); + + using (Dispatcher.CurrentDispatcher.DisableProcessing()) + { + this.AssertProcessingDisabled(); + } + + return Task.CompletedTask; + }); + + return Task.CompletedTask; + }); + } + + [StaFact] + public void WithPriority_MatchesDisableProcessingOutsideDelegate() + { + this.SimulateUIThread(delegate + { + JoinableTaskFactory? normalPriorityJtf = this.asyncPump.WithPriority(Dispatcher.CurrentDispatcher, DispatcherPriority.Normal); + using (Dispatcher.CurrentDispatcher.DisableProcessing()) + { + normalPriorityJtf.Run(delegate + { + this.AssertProcessingDisabled(); + + return Task.CompletedTask; + }); + } + + return Task.CompletedTask; + }); + } +#endif } diff --git a/test/Microsoft.VisualStudio.Threading.Tests/GenericParameterHelper.cs b/test/Microsoft.VisualStudio.Threading.Tests/GenericParameterHelper.cs index 6f935d0d6..0fdae9fde 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/GenericParameterHelper.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/GenericParameterHelper.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; diff --git a/test/Microsoft.VisualStudio.Threading.Tests/InternalUtilitiesTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/InternalUtilitiesTests.cs index a58ebcebc..bec84f2c4 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/InternalUtilitiesTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/InternalUtilitiesTests.cs @@ -1,10 +1,8 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; using System.Linq; -using Microsoft.VisualStudio.Threading; -using Xunit; public class InternalUtilitiesTests { diff --git a/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskAndAsyncReaderWriterLockTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskAndAsyncReaderWriterLockTests.cs index 04f98ff9e..33a233bfc 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskAndAsyncReaderWriterLockTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskAndAsyncReaderWriterLockTests.cs @@ -1,11 +1,8 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Threading.Tasks; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; public class JoinableTaskAndAsyncReaderWriterLockTests : TestBase { @@ -163,7 +160,7 @@ public async Task RunWithinUpgradeableReadLockThrows() try { this.asyncPump.Run(() => Task.CompletedTask); - Assert.False(true, "Expected InvalidOperationException not thrown."); + Assert.Fail("Expected InvalidOperationException not thrown."); } catch (InvalidOperationException) { @@ -190,7 +187,7 @@ public async Task RunWithinWriteLockThrows() try { this.asyncPump.Run(() => Task.CompletedTask); - Assert.True(false, "Expected InvalidOperationException not thrown."); + Assert.Fail("Expected InvalidOperationException not thrown."); } catch (InvalidOperationException) { diff --git a/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskCollectionTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskCollectionTests.cs index c860e0600..1cba90a05 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskCollectionTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskCollectionTests.cs @@ -1,12 +1,9 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Threading; using System.Threading.Tasks; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; public class JoinableTaskCollectionTests : JoinableTaskTestBase { diff --git a/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskContextNodeTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskContextNodeTests.cs index 7d4a98494..62393635a 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskContextNodeTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskContextNodeTests.cs @@ -1,13 +1,10 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; public class JoinableTaskContextNodeTests : JoinableTaskTestBase { diff --git a/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskContextTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskContextTests.cs index 3158fd9b3..b644fc158 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskContextTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskContextTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -7,9 +7,7 @@ using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; +using Microsoft; public class JoinableTaskContextTests : JoinableTaskTestBase { @@ -68,7 +66,7 @@ public void ReportHangOnRun() { Tuple? tuple = await hangQueue.DequeueAsync(ct); TimeSpan duration = tuple.Item1; - var iterations = tuple.Item2; + int iterations = tuple.Item2; Guid id = tuple.Item3; Assert.True(lastDuration == TimeSpan.Zero || lastDuration < duration); Assert.Equal(lastIteration + 1, iterations); @@ -410,8 +408,8 @@ public void GetHangReportProducesDgmlWithNamedJoinableCollections() this.Logger.WriteLine(report.Content); var dgml = XDocument.Parse(report.Content); IEnumerable? collectionLabels = from node in dgml.Root!.Element(XName.Get("Nodes", DgmlNamespace))!.Elements() - where node.Attribute(XName.Get("Category"))?.Value == "Collection" - select node.Attribute(XName.Get("Label"))?.Value; + where node.Attribute(XName.Get("Category"))?.Value == "Collection" + select node.Attribute(XName.Get("Label"))?.Value; Assert.Contains(collectionLabels, label => label == jtcName); return Task.CompletedTask; }); @@ -433,8 +431,8 @@ public void GetHangReportProducesDgmlWithMethodNameRequestingMainThread() this.Logger.WriteLine(report.Content); var dgml = XDocument.Parse(report.Content); IEnumerable? collectionLabels = from node in dgml.Root!.Element(XName.Get("Nodes", DgmlNamespace))!.Elements() - where node.Attribute(XName.Get("Category"))?.Value == "Task" - select node.Attribute(XName.Get("Label"))?.Value; + where node.Attribute(XName.Get("Category"))?.Value == "Task" + select node.Attribute(XName.Get("Label"))?.Value; Assert.Contains(collectionLabels, label => label.Contains(nameof(this.GetHangReportProducesDgmlWithMethodNameRequestingMainThread))); } @@ -456,8 +454,8 @@ public void GetHangReportProducesDgmlWithMethodNameYieldingOnMainThread() this.Logger.WriteLine(report.Content); var dgml = XDocument.Parse(report.Content); IEnumerable? collectionLabels = from node in dgml.Root!.Element(XName.Get("Nodes", DgmlNamespace))!.Elements() - where node.Attribute(XName.Get("Category"))?.Value == "Task" - select node.Attribute(XName.Get("Label"))?.Value; + where node.Attribute(XName.Get("Category"))?.Value == "Task" + select node.Attribute(XName.Get("Label"))?.Value; Assert.Contains(collectionLabels, label => label.Contains(nameof(this.YieldingMethodAsync))); }); } @@ -671,6 +669,39 @@ public void IsMainThreadBlockedTrueWhenAsyncOnOtherThreadBecomesSyncOnMainThread }); } + [Fact] + public void IsMainThreadBlockedFalseWhenTaskIsCompleted() + { + var nonBlockingStateObserved = new AsyncManualResetEvent(); + var nowBlocking = new AsyncManualResetEvent(); + + Task? checkTask = null; + this.Factory.Run( + async () => + { + checkTask = Task.Run( + async () => + { + nonBlockingStateObserved.Set(); + + await nowBlocking; + + Assert.False(this.Context.IsMainThreadMaybeBlocked()); + Assert.False(this.Context.IsMainThreadBlocked()); + }); + + Assert.True(this.Context.IsMainThreadMaybeBlocked()); + Assert.True(this.Context.IsMainThreadBlocked()); + + await nonBlockingStateObserved; + }); + + nowBlocking.Set(); + + Assert.NotNull(checkTask); + checkTask!.Wait(); + } + [Fact] public void RevertRelevanceDefaultValue() { @@ -685,6 +716,83 @@ public void Disposable() disposable.Dispose(); } + [Fact] + public void Ctor_ExplicitNullSyncContext() + { + this.SimulateUIThread(async delegate + { + Thread mainThread = Thread.CurrentThread; + Assumes.NotNull(SynchronizationContext.Current); + JoinableTaskContext jtc = JoinableTaskContext.CreateNoOpContext(); + Assert.True(jtc.IsNoOpContext); + await TaskScheduler.Default.SwitchTo(alwaysYield: true); // Get off the main thread. + Assert.NotSame(mainThread, Thread.CurrentThread); + + // Verify that switching to the main thread is a no-op. + Thread threadpoolThread = Thread.CurrentThread; + await jtc.Factory.SwitchToMainThreadAsync(this.TimeoutToken); + Assert.Same(threadpoolThread, Thread.CurrentThread); + }); + } + + [Fact] + public void Ctor_NullSyncContextArg_AmbientSyncContext() + { + this.SimulateUIThread(async delegate + { + Thread mainThread = Thread.CurrentThread; + Assumes.NotNull(SynchronizationContext.Current); + JoinableTaskContext jtc = new(null, null); + Assert.False(jtc.IsNoOpContext); + await TaskScheduler.Default.SwitchTo(alwaysYield: true); // Get off the main thread. + Assert.NotSame(mainThread, Thread.CurrentThread); + + // Verify that switching to the main thread works. + await jtc.Factory.SwitchToMainThreadAsync(this.TimeoutToken); + Assert.Same(mainThread, Thread.CurrentThread); + }); + } + + [Fact] + public void Ctor_Default() + { + this.SimulateUIThread(async delegate + { + Thread mainThread = Thread.CurrentThread; + Assumes.NotNull(SynchronizationContext.Current); + JoinableTaskContext jtc = new(); + Assert.False(jtc.IsNoOpContext); + await TaskScheduler.Default.SwitchTo(alwaysYield: true); // Get off the main thread. + Assert.NotSame(mainThread, Thread.CurrentThread); + + // Verify that switching to the main thread works. + await jtc.Factory.SwitchToMainThreadAsync(this.TimeoutToken); + Assert.Same(mainThread, Thread.CurrentThread); + }); + } + + [Fact] + public void Ctor_DefaultWithNoSyncContext() + { + this.SimulateUIThread(async delegate + { + await TaskScheduler.Default.SwitchTo(alwaysYield: true); // Get off the main thread. + + Thread currentThread = Thread.CurrentThread; + + Assumes.Null(SynchronizationContext.Current); + JoinableTaskContext jtc = new(); + Assert.True(jtc.IsNoOpContext); + + await TaskScheduler.Default.SwitchTo(); + Assert.Same(currentThread, Thread.CurrentThread); + + // Verify that switching to the main thread works. + await jtc.Factory.SwitchToMainThreadAsync(this.TimeoutToken); + Assert.Same(currentThread, Thread.CurrentThread); + }); + } + protected override JoinableTaskContext CreateJoinableTaskContext() { return new JoinableTaskContextDerived(); diff --git a/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskFactoryTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskFactoryTests.cs index 3540c0191..b7b8832e0 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskFactoryTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskFactoryTests.cs @@ -1,11 +1,8 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Threading.Tasks; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; public class JoinableTaskFactoryTests : JoinableTaskTestBase { @@ -141,6 +138,110 @@ public void SwitchToMainThreadAlwaysYield() }); } + [Fact] + public void DisableProcessing_ThrowsOutsideJoinableTask() + { + Assert.Throws(() => this.asyncPump.DisableProcessing()); + } + + [Fact] + public void DisableProcessing_InsideJoinableTask() + { + this.asyncPump.Run(delegate + { + using (this.asyncPump.DisableProcessing()) + { + } + + return Task.CompletedTask; + }); + } + + [Fact] + public void ProcessingDisabledOperation_Dispose_DoesNotThrowFromDefaultValue() + { + default(JoinableTaskFactory.ProcessingDisabledOperation).Dispose(); + } + +#if NETFRAMEWORK + [StaFact] + public void DisableProcessing() + { + this.asyncPump.Run(() => + { + this.AssertProcessingAllowed(); + + using (this.asyncPump.DisableProcessing()) + { + this.AssertProcessingDisabled(); + } + + this.AssertProcessingAllowed(); + return Task.CompletedTask; + }); + } + + [StaFact] + public void DisableProcessing_NestedProcessingDisabled() + { + this.asyncPump.Run(() => + { + using (this.asyncPump.DisableProcessing()) + { + using (this.asyncPump.DisableProcessing()) + { + this.AssertProcessingDisabled(); + } + + this.AssertProcessingDisabled(); + } + + this.AssertProcessingAllowed(); + return Task.CompletedTask; + }); + } + + [StaFact] + public void DisableProcessing_NestedTasks() + { + this.asyncPump.Run(() => + { + using (this.asyncPump.DisableProcessing()) + { + this.asyncPump.Run(() => + { + // Child JoinableTasks do not inherit the processing-disabled state of their parents. + this.AssertProcessingAllowed(); + + return Task.CompletedTask; + }); + } + + return Task.CompletedTask; + }); + } + + [StaFact] + public void DisableProcessing_RefCounted() + { + this.asyncPump.Run(() => + { + JoinableTaskFactory.ProcessingDisabledOperation first = this.asyncPump.DisableProcessing(); + JoinableTaskFactory.ProcessingDisabledOperation second = this.asyncPump.DisableProcessing(); + + // Dispose things in a FIFO order instead of a nested LIFO order. + // Processing should only be re-enabled after the last reference is disposed. + first.Dispose(); + this.AssertProcessingDisabled(); + second.Dispose(); + this.AssertProcessingAllowed(); + + return Task.CompletedTask; + }); + } + +#endif + /// /// A that allows a test to inject code /// in the main thread transition events. diff --git a/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskInternalsTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskInternalsTests.cs new file mode 100644 index 000000000..d13235468 --- /dev/null +++ b/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskInternalsTests.cs @@ -0,0 +1,384 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Threading; +using System.Threading.Tasks; + +public class JoinableTaskInternalsTests : JoinableTaskTestBase +{ + public JoinableTaskInternalsTests(ITestOutputHelper logger) + : base(logger) + { + } + + [Fact] + public void IsMainThreadBlockedByAnyJoinableTask_True() + { + Assert.False(JoinableTaskInternals.IsMainThreadBlockedByAnyJoinableTask(this.context)); + AsyncManualResetEvent mainThreadBlockerEvent = new AsyncManualResetEvent(false); + AsyncManualResetEvent backgroundThreadMonitorEvent = new AsyncManualResetEvent(false); + + // Start task to monitor IsMainThreadBlockedByAnyJoinableTask + Task monitorTask = Task.Run(async () => + { + await mainThreadBlockerEvent.WaitAsync(this.TimeoutToken); + + while (!JoinableTaskInternals.IsMainThreadBlockedByAnyJoinableTask(this.context)) + { + // Give the main thread time to enter a blocking state, if the test hasn't already timed out. + await Task.Delay(50, this.TimeoutToken); + } + + backgroundThreadMonitorEvent.Set(); + }); + + JoinableTask? joinable = this.asyncPump.RunAsync(async delegate + { + Assert.False(JoinableTaskInternals.IsMainThreadBlockedByAnyJoinableTask(this.context)); + await this.asyncPump.SwitchToMainThreadAsync(this.TimeoutToken); + + this.asyncPump.Run(async () => + { + await TaskScheduler.Default.SwitchTo(alwaysYield: true); + mainThreadBlockerEvent.Set(); + await backgroundThreadMonitorEvent.WaitAsync(this.TimeoutToken); + }); + }); + + joinable.Join(); + monitorTask.WaitWithoutInlining(throwOriginalException: true); + + Assert.False(JoinableTaskInternals.IsMainThreadBlockedByAnyJoinableTask(this.context)); + } + + [Fact] + public void IsMainThreadBlockedByAnyJoinableTask_False() + { + Assert.False(JoinableTaskInternals.IsMainThreadBlockedByAnyJoinableTask(this.context)); + ManualResetEventSlim backgroundThreadBlockerEvent = new(); + + JoinableTask? joinable = this.asyncPump.RunAsync(async delegate + { + Assert.False(JoinableTaskInternals.IsMainThreadBlockedByAnyJoinableTask(this.context)); + await TaskScheduler.Default.SwitchTo(alwaysYield: true); + + this.asyncPump.Run(async () => + { + backgroundThreadBlockerEvent.Set(); + + // Set a delay sufficient for the other thread to have noticed if IsMainThreadBlockedByAnyJoinableTask is true + // while we're suspended. + await Task.Delay(AsyncDelay); + }); + }); + + backgroundThreadBlockerEvent.Wait(UnexpectedTimeout); + + do + { + // Give the background thread time to enter a blocking state, if the test hasn't already timed out. + this.TimeoutToken.ThrowIfCancellationRequested(); + + // IsMainThreadBlockedByAnyJoinableTask should be false when a background thread is blocked. + Assert.False(JoinableTaskInternals.IsMainThreadBlockedByAnyJoinableTask(this.context)); + Thread.Sleep(10); + } + while (!joinable.IsCompleted); + + joinable.Join(); + + Assert.False(JoinableTaskInternals.IsMainThreadBlockedByAnyJoinableTask(this.context)); + } + + [Fact] + public void GetJoinableTaskTokenNullWhenNoTask() + { + Assert.Null(JoinableTaskInternals.GetJoinableTaskToken(null)); + Assert.Null(JoinableTaskInternals.GetJoinableTaskToken(this.context)); + } + + [Fact] + public void GetJoinableTaskTokenNotNullWhenTaskRunning() + { + JoinableTask? joinable = this.asyncPump.RunAsync(async delegate + { + Assert.NotNull(JoinableTaskInternals.GetJoinableTaskToken(this.context)); + await TaskScheduler.Default.SwitchTo(alwaysYield: true); + Assert.NotNull(JoinableTaskInternals.GetJoinableTaskToken(this.context)); + }); + + joinable.Join(); + Assert.Null(JoinableTaskInternals.GetJoinableTaskToken(this.context)); + } + + [Fact] + public void IsMainThreadMabyeBlockedFalseNullToken() + { + JoinableTaskInternals.JoinableTaskToken? token = null; + Assert.False(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + } + + [Fact] + public void IsMainThreadMabyeBlockedFalseWithNoTask() + { + JoinableTaskInternals.JoinableTaskToken? token = JoinableTaskInternals.GetJoinableTaskToken(this.context); + Assert.False(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + } + + [Fact] + public void IsMainThreadBlockedFalseWhenAsync() + { + JoinableTaskInternals.JoinableTaskToken? token; + + JoinableTask? joinable = this.asyncPump.RunAsync(async delegate + { + token = JoinableTaskInternals.GetJoinableTaskToken(this.context); + Assert.False(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + await Task.Yield(); + Assert.False(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + this.testFrame.Continue = false; + }); + + this.PushFrame(); + joinable.Join(); // rethrow exceptions + } + + [Fact] + public void IsMainThreadBlockedTrueWhenAsyncBecomesBlocking() + { + JoinableTask? joinable = this.asyncPump.RunAsync(async delegate + { + JoinableTaskInternals.JoinableTaskToken? token = JoinableTaskInternals.GetJoinableTaskToken(this.context); + Assert.False(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + + await Task.Yield(); + Assert.True(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); // we're now running on top of Join() + + await TaskScheduler.Default.SwitchTo(alwaysYield: true); + Assert.True(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); // although we're on background thread, we're blocking main thread. + + await this.asyncPump.RunAsync(async delegate + { + Assert.True(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + await Task.Yield(); + Assert.True(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + + await this.asyncPump.SwitchToMainThreadAsync(); + Assert.True(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + }); + }); + + joinable.Join(); + } + + [Fact] + public void IsMainThreadBlockedTrueWhenAsyncBecomesBlockingWithNestedTask() + { + JoinableTask? joinable = this.asyncPump.RunAsync(async delegate + { + JoinableTaskInternals.JoinableTaskToken? token = JoinableTaskInternals.GetJoinableTaskToken(this.context); + + Assert.False(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + await Task.Yield(); + + Assert.False(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + + await TaskScheduler.Default.SwitchTo(alwaysYield: true); + Assert.False(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + + await this.asyncPump.RunAsync(async delegate + { + Assert.False(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + + // Now release the message pump so we hit the Join() call + await this.asyncPump.SwitchToMainThreadAsync(); + this.testFrame.Continue = false; + await Task.Yield(); + + // From now on, we're blocking. + Assert.True(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + + await TaskScheduler.Default.SwitchTo(alwaysYield: true); + Assert.True(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + }); + }); + + this.PushFrame(); // for duration of this, it appears to be non-blocking. + joinable.Join(); + } + + [Fact] + public void IsMainThreadBlockedTrueWhenOriginallySync() + { + this.asyncPump.Run(async delegate + { + JoinableTaskInternals.JoinableTaskToken? token = JoinableTaskInternals.GetJoinableTaskToken(this.context); + + Assert.True(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + await Task.Yield(); + + Assert.True(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + + await TaskScheduler.Default.SwitchTo(alwaysYield: true); + Assert.True(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + + await this.asyncPump.RunAsync(async delegate + { + Assert.True(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + + await Task.Yield(); + Assert.True(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + + await this.asyncPump.SwitchToMainThreadAsync(); + Assert.True(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + }); + }); + } + + [Fact] + public void IsMainThreadBlockedFalseWhenSyncBlockingOtherThread() + { + Task.Run(delegate + { + this.asyncPump.Run(async delegate + { + JoinableTaskInternals.JoinableTaskToken? token = JoinableTaskInternals.GetJoinableTaskToken(this.context); + + Assert.False(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + + await Task.Yield(); + Assert.False(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + }); + }).WaitWithoutInlining(throwOriginalException: true); + } + + [Fact] + public void IsMainThreadBlockedTrueWhenAsyncOnOtherThreadBecomesSyncOnMainThread() + { + var nonBlockingStateObserved = new AsyncManualResetEvent(); + var nowBlocking = new AsyncManualResetEvent(); + JoinableTask? joinableTask = null; + Task.Run(delegate + { + joinableTask = this.asyncPump.RunAsync(async delegate + { + JoinableTaskInternals.JoinableTaskToken? token = JoinableTaskInternals.GetJoinableTaskToken(this.context); + Assert.False(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + + nonBlockingStateObserved.Set(); + await Task.Yield(); + await nowBlocking; + + Assert.True(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + }); + }).Wait(); + + this.asyncPump.Run(async delegate + { + await nonBlockingStateObserved; + joinableTask!.JoinAsync().Forget(); + nowBlocking.Set(); + }); + } + + [Fact] + public void IsMainThreadBlockedFalseWhenTaskIsCompleted() + { + var nonBlockingStateObserved = new AsyncManualResetEvent(); + var nowBlocking = new AsyncManualResetEvent(); + + Task? checkTask = null; + this.asyncPump.Run( + async () => + { + JoinableTaskInternals.JoinableTaskToken? token = JoinableTaskInternals.GetJoinableTaskToken(this.context); + + checkTask = Task.Run( + async () => + { + nonBlockingStateObserved.Set(); + + await nowBlocking; + + Assert.False(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + }); + + Assert.True(JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + + await nonBlockingStateObserved; + }); + + nowBlocking.Set(); + + Assert.NotNull(checkTask); + checkTask!.Wait(); + } + + [Fact] + public void IsMainThreadMaybeBlockedEqualsJoinableTaskContextIsMainThreadMaybeBlocked_NoTask() + { + JoinableTaskInternals.JoinableTaskToken? token = JoinableTaskInternals.GetJoinableTaskToken(this.context); + Assert.Equal(this.context.IsMainThreadMaybeBlocked(), JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + } + + [Fact] + public void IsMainThreadMaybeBlockedEqualsJoinableTaskContextIsMainThreadMaybeBlocked_Async() + { + JoinableTaskInternals.JoinableTaskToken? token; + JoinableTask? joinable = this.asyncPump.RunAsync(async delegate + { + token = JoinableTaskInternals.GetJoinableTaskToken(this.context); + Assert.Equal(this.context.IsMainThreadMaybeBlocked(), JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + await TaskScheduler.Default.SwitchTo(alwaysYield: true); + Assert.Equal(this.context.IsMainThreadMaybeBlocked(), JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + + await this.asyncPump.RunAsync(async delegate + { + Assert.Equal(this.context.IsMainThreadMaybeBlocked(), JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + await Task.Yield(); + + Assert.Equal(this.context.IsMainThreadMaybeBlocked(), JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + + await this.asyncPump.SwitchToMainThreadAsync(); + Assert.Equal(this.context.IsMainThreadMaybeBlocked(), JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + }); + }); + + joinable.Join(); + + token = JoinableTaskInternals.GetJoinableTaskToken(this.context); + Assert.Equal(this.context.IsMainThreadMaybeBlocked(), JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + } + + [Fact] + public void IsMainThreadMaybeBlockedEqualsJoinableTaskContextIsMainThreadMaybeBlocked_Sync() + { + this.asyncPump.Run(async delegate + { + JoinableTaskInternals.JoinableTaskToken? token = JoinableTaskInternals.GetJoinableTaskToken(this.context); + + Assert.Equal(this.context.IsMainThreadMaybeBlocked(), JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + await TaskScheduler.Default.SwitchTo(alwaysYield: true); + + Assert.Equal(this.context.IsMainThreadMaybeBlocked(), JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + }); + } + + [Fact] + public void DifferentTokenIsMainThreadMaybeBlockedNotEqualJoinableTaskContextIsMainThreadMaybeBlocked() + { + JoinableTaskInternals.JoinableTaskToken? token = null; + this.asyncPump.Run(async delegate + { + token = JoinableTaskInternals.GetJoinableTaskToken(this.context); + await TaskScheduler.Default.SwitchTo(alwaysYield: true); + }); + + this.asyncPump.Run(async delegate + { + Assert.NotEqual(this.context.IsMainThreadMaybeBlocked(), JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + await TaskScheduler.Default.SwitchTo(alwaysYield: true); + Assert.NotEqual(this.context.IsMainThreadMaybeBlocked(), JoinableTaskInternals.IsMainThreadMaybeBlocked(token)); + }); + } +} diff --git a/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskTestBase.cs b/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskTestBase.cs index 3da5d874e..bd64031a9 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskTestBase.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskTestBase.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -9,8 +9,6 @@ using System.Threading.Tasks; using System.Xml.Linq; using Microsoft; -using Microsoft.VisualStudio.Threading; -using Xunit.Abstractions; public abstract class JoinableTaskTestBase : TestBase { @@ -98,4 +96,30 @@ protected void PushFrameTillQueueIsEmpty() this.dispatcherContext.Post(s => this.testFrame.Continue = false, null); this.PushFrame(); } + +#if NETFRAMEWORK + protected void AssertProcessingDisabled() + { + Assert.SkipUnless(MightCoWaitBeUsed, "DisableProcessing has no effect in this environment."); + + // For this check to work, we need to be on the main thread. + Assert.True(this.asyncPump.Context.IsOnMainThread); + Assert.Equal(ApartmentState.STA, Thread.CurrentThread.GetApartmentState()); + + using CoWaitMainThreadTransition transition = new(); + Assert.False(transition.Wait(ExpectedTimeout)); + } + + protected void AssertProcessingAllowed() + { + Assert.SkipUnless(MightCoWaitBeUsed, "DisableProcessing has no effect in this environment."); + + // For this check to work, we need to be on the main thread. + Assert.True(this.asyncPump.Context.IsOnMainThread); + Assert.Equal(ApartmentState.STA, Thread.CurrentThread.GetApartmentState()); + + using CoWaitMainThreadTransition transition = new(); + Assert.True(transition.Wait(UnexpectedTimeout)); + } +#endif } diff --git a/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskTests.cs index b6387b1d8..7264640fc 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -10,9 +10,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; public class JoinableTaskTests : JoinableTaskTestBase { @@ -48,7 +45,7 @@ public void RunFuncOfTaskOfTMTA() [Fact] public void LeaveAndReturnToMainThread() { - var fullyCompleted = false; + bool fullyCompleted = false; this.asyncPump.Run(async delegate { Assert.Equal(this.originalThreadManagedId, Environment.CurrentManagedThreadId); @@ -108,13 +105,13 @@ public void SwitchToMainThreadAsyncContributesToHangReportsAndCollections() // Verify here that pendingTasks includes one task. Assert.Equal(1, this.GetPendingTasksCount()); - Assert.Single(this.joinableCollection); + Assert.Single(this.joinableCollection!); // Now let the request proceed through. this.PushFrame(); Assert.Equal(0, this.GetPendingTasksCount()); - Assert.Empty(this.joinableCollection); + Assert.Empty(this.joinableCollection!); if (delegateFailure is object) { @@ -238,7 +235,7 @@ public void SwitchToMainThreadAsyncTransitionsCanSeeAsyncLocals() Exception? delegateFailure = null; var asyncLocal = new System.Threading.AsyncLocal(); - var asyncLocalValue = new object(); + object asyncLocalValue = new object(); // The point of this test is to verify that the transitioning/transitioned // methods on the JoinableTaskFactory can see into the AsyncLocal.Value @@ -293,7 +290,7 @@ public void SwitchToMainThreadCancellable() try { await this.asyncPump.SwitchToMainThreadAsync(cts.Token); - Assert.True(false, "Expected OperationCanceledException not thrown."); + Assert.Fail("Expected OperationCanceledException not thrown."); } catch (OperationCanceledException) { @@ -306,6 +303,21 @@ public void SwitchToMainThreadCancellable() Assert.True(task.Wait(TestTimeout * 3), "Test timed out."); } + [Fact] + public void SwitchToMainThreadNoThrowCancellable() + { + var task = Task.Run(async delegate + { + var cts = new CancellationTokenSource(AsyncDelay); + await this.asyncPump.SwitchToMainThreadAsync(cts.Token).NoThrowAwaitable(); + + Assert.Null(SynchronizationContext.Current); + Assert.NotEqual(this.originalThreadManagedId, Environment.CurrentManagedThreadId); + }); + + Assert.True(task.Wait(TestTimeout * 3), "Test timed out."); + } + [Fact] public void SwitchToMainThreadCancellableWithinRun() { @@ -322,7 +334,7 @@ public void SwitchToMainThreadCancellableWithinRun() }); } }); - Assert.True(false, "Expected OperationCanceledException not thrown."); + Assert.Fail("Expected OperationCanceledException not thrown."); } catch (OperationCanceledException) { @@ -364,6 +376,26 @@ public void SwitchToMainThreadAsync_Await_Canceled_CapturesExecutionContext() task.Wait(this.TimeoutToken); } + [Fact] + public void SwitchToMainThreadAsync_NoThrowAwait_Canceled_CapturesExecutionContext() + { + var factory = (DerivedJoinableTaskFactory)this.asyncPump; + var cts = new CancellationTokenSource(); + var transitionRequested = new ManualResetEventSlim(); + factory.TransitioningToMainThreadCallback = jt => transitionRequested.Set(); + var task = Task.Run(async delegate + { + var asyncLocal = new System.Threading.AsyncLocal(); + asyncLocal.Value = "expected"; + await this.asyncPump.SwitchToMainThreadAsync(cts.Token).NoThrowAwaitable(); + Assert.NotEqual(this.originalThreadManagedId, Environment.CurrentManagedThreadId); + Assert.Equal("expected", asyncLocal.Value); + }); + transitionRequested.Wait(); + cts.Cancel(); + task.Wait(this.TimeoutToken); + } + [Fact] public void SwitchToMainThreadAsync_UnsafeOnCompleted_DoesNotCaptureExecutionContext() { @@ -489,6 +521,21 @@ public void SwitchToMainThread_PrecanceledOnMainThread() }); } + [Fact] + public void SwitchToMainThread_PrecanceledOnMainThread_NoThrow() + { + this.SimulateUIThread(async delegate + { + JoinableTaskFactory.MainThreadAwaiter awaiter = this.asyncPump.SwitchToMainThreadAsync(new CancellationToken(true)).NoThrowAwaitable().GetAwaiter(); + Assert.True(awaiter.IsCompleted); + awaiter.GetResult(); + + // Verify that the SynchronizationContext remains such that we stay on the main thread after yielding. + await Task.Yield(); + Assert.True(this.context.IsOnMainThread); + }); + } + [Fact] public void SwitchToMainThread_PrecanceledOnMainThread_StillYieldsWhenRequired() { @@ -514,6 +561,31 @@ public void SwitchToMainThread_PrecanceledOnMainThread_StillYieldsWhenRequired() }); } + [Fact] + public void SwitchToMainThread_PrecanceledOnMainThread_StillYieldsWhenRequired_NoThrow() + { + this.SimulateUIThread(async delegate + { + JoinableTaskFactory.MainThreadAwaiter awaiter = this.asyncPump.SwitchToMainThreadAsync(alwaysYield: true, new CancellationToken(true)).NoThrowAwaitable().GetAwaiter(); + Assert.False(awaiter.IsCompleted); + var testResult = new TaskCompletionSource(); + awaiter.OnCompleted(delegate + { + try + { + awaiter.GetResult(); + Assert.Equal(this.context.IsOnMainThread, SynchronizationContext.Current is object); + testResult.SetResult(null); + } + catch (Exception ex) + { + testResult.SetException(ex); + } + }); + await testResult.Task.WithCancellation(this.TimeoutToken); + }); + } + [Fact] public void SwitchToMainThreadAsync_CompletesSynchronouslyWhenPreCanceledOffMainThread() { @@ -531,6 +603,22 @@ public void SwitchToMainThreadAsync_CompletesSynchronouslyWhenPreCanceledOffMain }); } + [Fact] + public void SwitchToMainThreadAsync_CompletesSynchronouslyWhenPreCanceledOffMainThread_NoThrow() + { + this.SimulateUIThread(delegate + { + return Task.Run(delegate + { + var precanceled = new CancellationToken(canceled: true); + JoinableTaskFactory.MainThreadAwaiter awaiter = this.asyncPump.SwitchToMainThreadAsync(precanceled).NoThrowAwaitable().GetAwaiter(); + Assert.True(awaiter.IsCompleted); + awaiter.GetResult(); + Assert.Null(SynchronizationContext.Current); + }); + }); + } + [Fact] public void SwitchToMainThreadAsync_CanceledToBackgroundThreadWithSyncContext() { @@ -604,6 +692,36 @@ public void SwitchToMainThreadAsync_ThrowsOnCancellationAfterReachingMainThread( }); } + [Fact] + public void SwitchToMainThreadAsync_NoThrowOnCancellationAfterReachingMainThread() + { + this.SimulateUIThread(delegate + { + return Task.Run(async delegate + { + var cts = new CancellationTokenSource(); + JoinableTaskFactory.MainThreadAwaiter awaiter = this.asyncPump.SwitchToMainThreadAsync(cts.Token).NoThrowAwaitable().GetAwaiter(); + Assert.False(awaiter.IsCompleted); + var testResult = new TaskCompletionSource(); + awaiter.OnCompleted(delegate + { + try + { + Assert.True(this.context.IsOnMainThread); + cts.Cancel(); + awaiter.GetResult(); + testResult.SetResult(null); + } + catch (Exception ex) + { + testResult.SetException(ex); + } + }); + await testResult.Task.WithCancellation(this.TimeoutToken); + }); + }); + } + /// /// Verify that if the was initialized /// without a whose @@ -1813,7 +1931,7 @@ public void SynchronousTaskStackMaintainedCorrectly() { this.asyncPump.Run(async delegate { - this.asyncPump.Run(() => Task.FromResult(true)); + this.asyncPump.Run(() => Task.FromResult(true)); await Task.Yield(); }); } @@ -2114,8 +2232,8 @@ public void BeginAsyncWithResultOnMTAKicksOffOtherAsyncPumpWorkCanCompleteSynchr }).Result; Assert.False(joinable.Task.IsCompleted); - var result = joinable.Join(); - Assert.Equal(5, result); + int result = joinable.Join(); + Assert.Equal(5, result); Assert.True(taskFinished); Assert.True(joinable.Task.IsCompleted); } @@ -2147,7 +2265,7 @@ public void Join_AlreadyCompletedWithPrecanceledArgument() [Fact] public void Join_AlreadyCompletedWithPrecanceledArgument_Generic() { - JoinableTask jt = this.asyncPump.RunAsync(() => Task.FromResult(0)); + JoinableTask jt = this.asyncPump.RunAsync(() => Task.FromResult(0)); Assert.Throws(() => jt.Join(new CancellationToken(canceled: true))); } @@ -2161,7 +2279,7 @@ public async Task JoinAsync_AlreadyCompletedWithPrecanceledArgument() [Fact] public async Task JoinAsync_AlreadyCompletedWithPrecanceledArgument_Generic() { - JoinableTask jt = this.asyncPump.RunAsync(() => Task.FromResult(0)); + JoinableTask jt = this.asyncPump.RunAsync(() => Task.FromResult(0)); await Assert.ThrowsAsync(() => jt.JoinAsync(new CancellationToken(canceled: true))); } @@ -2379,7 +2497,7 @@ public void PostedMessagesAlsoSentToDispatcher() if (ex is object) { - Assert.True(false, $"Posted message threw an exception: {ex}"); + Assert.Fail($"Posted message threw an exception: {ex}"); } return Task.CompletedTask; @@ -2510,7 +2628,7 @@ public void JoinWorkStealingRetainsThreadAffinityUI() public void JoinWorkStealingRetainsThreadAffinityBackground() { bool synchronousCompletionStarting = false; - var asyncTask = Task.Run(delegate + Task asyncTask = Task.Run(delegate { return this.asyncPump.RunAsync(async delegate { @@ -2856,7 +2974,7 @@ await loPriFactory.RunAsync(async delegate hiPriFactory.DoModalLoopTillEmptyAndTaskCompleted(outer.Task, this.TimeoutToken); } - [SkippableFact] + [Fact] public void NestedFactoriesCanBeCollected() { WeakReference weakOuterFactory = this.NestedFactoriesCanBeCollected_Helper(); @@ -3156,6 +3274,7 @@ public void MitigationAgainstBadSyncContextOnMainThread() #if ISOLATED_TEST_SUPPORT [Fact, Trait("Stress", "true")] [Trait("GC", "true")] + [Trait("TestCategory", "FailsInCloudTest")] public void SwitchToMainThreadMemoryLeak() { if (this.ExecuteInIsolation()) @@ -3172,6 +3291,7 @@ async delegate [Fact, Trait("Stress", "true")] [Trait("GC", "true")] + [Trait("TestCategory", "FailsInCloudTest")] public void SwitchToMainThreadMemoryLeakWithCancellationToken() { if (this.ExecuteInIsolation()) @@ -3368,7 +3488,7 @@ public void RunAsyncExceptionsCapturedInResult() try { awaiter.GetResult(); - Assert.True(false, "Expected exception not rethrown."); + Assert.Fail("Expected exception not rethrown."); } catch (InvalidOperationException ex) { @@ -3390,7 +3510,7 @@ public void RunAsyncOfTExceptionsCapturedInResult() try { awaiter.GetResult(); - Assert.True(false, "Expected exception not rethrown."); + Assert.Fail("Expected exception not rethrown."); } catch (InvalidOperationException ex) { @@ -3453,6 +3573,7 @@ public void RunAsyncWithYieldingDelegateNestedInRunOverhead() } [Fact] + [Trait("TestCategory", "FailsInCloudTest")] public void SwitchToMainThreadShouldNotLeakJoinableTaskWhenGetResultRunsFirst() { WeakReference weakResult = this.SwitchToMainThreadShouldNotLeakJoinableTaskWhenGetResultRunsFirst_Helper(); @@ -3641,7 +3762,7 @@ public void JoinAsyncShouldCompleteWithoutUIThreadAfterCancellation() // We expect to be able to block on the UI thread and the Task complete. // In completing, it will throw a TaskCanceledException, wrapped by an // AggregateException. If it 'hangs', it will timeout, returning false. - Assert.Throws(() => joinTask.Wait(AsyncDelay)); + Assert.Throws(() => joinTask.Wait(UnexpectedTimeout)); } [Fact] @@ -3882,7 +4003,7 @@ public void JoinableTaskOfT_TaskPropertyBeforeReturning() public void IsCompletedTrueDoesNotLock() { using var context = new JoinableTaskContext(); - var syncContextLock = typeof(JoinableTaskContext).GetProperty("SyncContextLock", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(context)!; + object syncContextLock = typeof(JoinableTaskContext).GetProperty("SyncContextLock", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(context)!; Assert.NotNull(syncContextLock); JoinableTask joinableTask = context.Factory.RunAsync(() => Task.CompletedTask); @@ -3914,7 +4035,7 @@ public void IsCompletedTrueDoesNotLock() public void JoinCompletedDoesNotLock() { using var context = new JoinableTaskContext(); - var syncContextLock = typeof(JoinableTaskContext).GetProperty("SyncContextLock", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(context)!; + object syncContextLock = typeof(JoinableTaskContext).GetProperty("SyncContextLock", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(context)!; Assert.NotNull(syncContextLock); JoinableTask joinableTask = context.Factory.RunAsync(() => Task.CompletedTask); @@ -3946,7 +4067,7 @@ public void JoinCompletedDoesNotLock() public void JoinAsyncCompletedDoesNotLock() { using var context = new JoinableTaskContext(); - var syncContextLock = typeof(JoinableTaskContext).GetProperty("SyncContextLock", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(context)!; + object syncContextLock = typeof(JoinableTaskContext).GetProperty("SyncContextLock", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(context)!; Assert.NotNull(syncContextLock); JoinableTask joinableTask = context.Factory.RunAsync(() => Task.CompletedTask); @@ -3982,7 +4103,7 @@ public void JoinAsyncCompletedDoesNotLock() public void GetAwaiterCompletedDoesNotLock() { using var context = new JoinableTaskContext(); - var syncContextLock = typeof(JoinableTaskContext).GetProperty("SyncContextLock", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(context)!; + object syncContextLock = typeof(JoinableTaskContext).GetProperty("SyncContextLock", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(context)!; Assert.NotNull(syncContextLock); JoinableTask joinableTask = context.Factory.RunAsync(() => Task.CompletedTask); @@ -4016,7 +4137,7 @@ public void GetAwaiterCompletedDoesNotLock() public void JoinCompletedTDoesNotLock() { using var context = new JoinableTaskContext(); - var syncContextLock = typeof(JoinableTaskContext).GetProperty("SyncContextLock", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(context)!; + object syncContextLock = typeof(JoinableTaskContext).GetProperty("SyncContextLock", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(context)!; Assert.NotNull(syncContextLock); JoinableTask joinableTask = context.Factory.RunAsync(() => Task.FromResult(0)); @@ -4048,7 +4169,7 @@ public void JoinCompletedTDoesNotLock() public void JoinAsyncCompletedTDoesNotLock() { using var context = new JoinableTaskContext(); - var syncContextLock = typeof(JoinableTaskContext).GetProperty("SyncContextLock", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(context)!; + object syncContextLock = typeof(JoinableTaskContext).GetProperty("SyncContextLock", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(context)!; Assert.NotNull(syncContextLock); JoinableTask joinableTask = context.Factory.RunAsync(() => Task.FromResult(0)); @@ -4086,7 +4207,7 @@ public void JoinAsyncCompletedTDoesNotLock() public void GetAwaiterCompletedTDoesNotLock() { using var context = new JoinableTaskContext(); - var syncContextLock = typeof(JoinableTaskContext).GetProperty("SyncContextLock", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(context)!; + object syncContextLock = typeof(JoinableTaskContext).GetProperty("SyncContextLock", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(context)!; Assert.NotNull(syncContextLock); JoinableTask joinableTask = context.Factory.RunAsync(() => Task.FromResult(0)); @@ -4367,7 +4488,7 @@ private WeakReference NestedFactoriesCanBeCollected_Helper() }); outerFactory.DoModalLoopTillEmptyAndTaskCompleted(outer.Task, this.TimeoutToken); - Skip.IfNot(outer.IsCompleted, "this is a product defect, but this test assumes this works to test something else."); + Assert.SkipUnless(outer.IsCompleted, "this is a product defect, but this test assumes this works to test something else."); // Allow the dispatcher to drain all messages that may be holding references. SynchronizationContext.Current!.Post(s => this.testFrame.Continue = false, null); @@ -4396,7 +4517,7 @@ private WeakReference JoinableTaskReleasedBySyncContextAfterCompletion_Helper(ou private void RunFuncOfTaskHelper() { - var initialThread = Environment.CurrentManagedThreadId; + int initialThread = Environment.CurrentManagedThreadId; this.asyncPump.Run(async delegate { Assert.Equal(initialThread, Environment.CurrentManagedThreadId); @@ -4407,7 +4528,7 @@ private void RunFuncOfTaskHelper() private void RunFuncOfTaskOfTHelper() { - var initialThread = Environment.CurrentManagedThreadId; + int initialThread = Environment.CurrentManagedThreadId; var expectedResult = new GenericParameterHelper(); GenericParameterHelper actualResult = this.asyncPump.Run(async delegate { @@ -4422,7 +4543,7 @@ private void RunFuncOfTaskOfTHelper() /// /// Writes out a DGML graph of pending tasks and collections to the test context. /// - /// A specific context to collect data from; null will use this.context. + /// A specific context to collect data from; will use this.context. private void PrintActiveTasksReport(JoinableTaskContext? context = null) { context = context ?? this.context; diff --git a/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskTokenTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskTokenTests.cs new file mode 100644 index 000000000..7d2e07360 --- /dev/null +++ b/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskTokenTests.cs @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Threading; +using System.Threading.Tasks; + +public class JoinableTaskTokenTests : JoinableTaskTestBase +{ + public JoinableTaskTokenTests(ITestOutputHelper logger) + : base(logger) + { + } + + [Fact] + public void Capture_NoContext() + { + Assert.Null(this.context.Capture()); + } + + [Fact] + public void Capture_InsideRunContext() + { + string? token = null; + this.asyncPump.Run(delegate + { + token = this.context.Capture(); + return Task.CompletedTask; + }); + Assert.NotNull(token); + this.Logger.WriteLine($"Token {token}"); + } + + [Fact] + public void Capture_InsideRunContextWithoutSyncContext() + { + SynchronizationContext.SetSynchronizationContext(null); + JoinableTaskContext asyncPump = new(); + asyncPump.Factory.Run(delegate + { + Assert.Null(asyncPump.Capture()); + return Task.CompletedTask; + }); + } + + [Fact] + public void Capture_InheritsFromParent() + { + const string UnknownParent = "97f67c3ce2c74dc6bdb1d8a58edb9176:13"; + string? token = null; + this.asyncPump.RunAsync( + delegate + { + token = this.context.Capture(); + return Task.CompletedTask; + }, + UnknownParent, + JoinableTaskCreationOptions.None).Join(); + Assert.NotNull(token); + this.Logger.WriteLine($"Token {token}"); + Assert.Contains(UnknownParent, token); + Assert.True(token.Length > UnknownParent.Length); + } + + [Fact] + public void Capture_ReplacesParent() + { + AsyncManualResetEvent unblockParent = new(); + string? parentToken = null; + JoinableTask parent = this.asyncPump.RunAsync( + async delegate + { + parentToken = this.context.Capture(); + await unblockParent; + }); + Assert.NotNull(parentToken); + this.Logger.WriteLine($"Parent: {parentToken}"); + + string? childToken = null; + this.asyncPump.RunAsync( + delegate + { + childToken = this.context.Capture(); + unblockParent.Set(); + return Task.CompletedTask; + }, + parentToken, + JoinableTaskCreationOptions.None).Join(); + Assert.NotNull(childToken); + this.Logger.WriteLine($"Child: {childToken}"); + + // Assert that the child token *replaced* the parent token since they both came from the same context. + Assert.Equal(parentToken.Length, childToken.Length); + Assert.NotEqual(parentToken, childToken); + } + + [Fact] + public void RunAsync_AfterParentCompletes() + { + string? token = null; + this.asyncPump.Run(delegate + { + token = this.context.Capture(); + return Task.CompletedTask; + }); + Assert.NotNull(token); + this.Logger.WriteLine($"Token: {token}"); + + this.asyncPump.RunAsync( + () => Task.CompletedTask, + token, + JoinableTaskCreationOptions.None).Join(); + } + + [Theory, PairwiseData] + public async Task RunAsync_AvoidsDeadlockWithParent(bool includeOtherContexts) + { + string? parentToken = includeOtherContexts ? "abc:dead;ghi:coffee" : null; + TaskCompletionSource tokenSource = new(); + AsyncManualResetEvent releaseOuterTask = new(); + + JoinableTask outerTask = this.asyncPump.RunAsync( + async delegate + { + try + { + tokenSource.SetResult(this.context.Capture()); + await releaseOuterTask; + } + catch (Exception ex) + { + tokenSource.SetException(ex); + } + }, + parentToken, + JoinableTaskCreationOptions.None); + + string? token = await tokenSource.Task; + Assert.NotNull(token); + this.Logger.WriteLine($"Token: {token}"); + if (parentToken is not null) + { + Assert.Contains(parentToken, token); + token += ";even=feed"; + this.Logger.WriteLine($"Token (modified): {token}"); + } + + JoinableTask innerTask = this.asyncPump.RunAsync( + async delegate + { + await Task.Yield(); + releaseOuterTask.Set(); + }, + token, + JoinableTaskCreationOptions.None); + + // Sync block the main thread using the outer task. + // No discernable dependency chain exists from outer to inner task, + // yet one subtly exists. Only the serialized context should allow inner + // to complete and thus unblock outer and avoid a deadlock. + outerTask.Join(this.TimeoutToken); + } + + [Theory, PairwiseData] + public async Task RunAsyncOfT_AvoidsDeadlockWithParent(bool includeOtherContexts) + { + string? parentToken = includeOtherContexts ? "abc:dead;ghi:coffee" : null; + TaskCompletionSource tokenSource = new(); + AsyncManualResetEvent releaseOuterTask = new(); + + JoinableTask outerTask = this.asyncPump.RunAsync( + async delegate + { + try + { + tokenSource.SetResult(this.context.Capture()); + await releaseOuterTask; + } + catch (Exception ex) + { + tokenSource.SetException(ex); + } + + return true; + }, + parentToken, + JoinableTaskCreationOptions.None); + + string? token = await tokenSource.Task; + Assert.NotNull(token); + this.Logger.WriteLine($"Token: {token}"); + if (parentToken is not null) + { + Assert.Contains(parentToken, token); + token += ";even=feed"; + this.Logger.WriteLine($"Token (modified): {token}"); + } + + JoinableTask innerTask = this.asyncPump.RunAsync( + async delegate + { + await Task.Yield(); + releaseOuterTask.Set(); + return true; + }, + token, + JoinableTaskCreationOptions.None); + + // Sync block the main thread using the outer task. + // No discernable dependency chain exists from outer to inner task, + // yet one subtly exists. Only the serialized context should allow inner + // to complete and thus unblock outer and avoid a deadlock. + outerTask.Join(this.TimeoutToken); + } +} diff --git a/test/Microsoft.VisualStudio.Threading.Tests/ListOfOftenOneTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/ListOfOftenOneTests.cs index cc7c7ffcc..539f88190 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/ListOfOftenOneTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/ListOfOftenOneTests.cs @@ -1,10 +1,7 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Linq; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; public class ListOfOftenOneTests : TestBase { @@ -34,11 +31,11 @@ public void EnumerationOfOne() using (ListOfOftenOne.Enumerator enumerator = this.list.GetEnumerator()) { Assert.True(enumerator.MoveNext()); - Assert.Equal(1, enumerator.Current.Data); + Assert.Equal(1, enumerator.Current.Data); Assert.False(enumerator.MoveNext()); enumerator.Reset(); Assert.True(enumerator.MoveNext()); - Assert.Equal(1, enumerator.Current.Data); + Assert.Equal(1, enumerator.Current.Data); Assert.False(enumerator.MoveNext()); } } @@ -51,15 +48,15 @@ public void EnumerationOfTwo() using (ListOfOftenOne.Enumerator enumerator = this.list.GetEnumerator()) { Assert.True(enumerator.MoveNext()); - Assert.Equal(1, enumerator.Current.Data); + Assert.Equal(1, enumerator.Current.Data); Assert.True(enumerator.MoveNext()); - Assert.Equal(2, enumerator.Current.Data); + Assert.Equal(2, enumerator.Current.Data); Assert.False(enumerator.MoveNext()); enumerator.Reset(); Assert.True(enumerator.MoveNext()); - Assert.Equal(1, enumerator.Current.Data); + Assert.Equal(1, enumerator.Current.Data); Assert.True(enumerator.MoveNext()); - Assert.Equal(2, enumerator.Current.Data); + Assert.Equal(2, enumerator.Current.Data); Assert.False(enumerator.MoveNext()); } } diff --git a/test/Microsoft.VisualStudio.Threading.Tests/Microsoft.VisualStudio.Threading.Tests.csproj b/test/Microsoft.VisualStudio.Threading.Tests/Microsoft.VisualStudio.Threading.Tests.csproj index 6e7a831c3..d92df35cd 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/Microsoft.VisualStudio.Threading.Tests.csproj +++ b/test/Microsoft.VisualStudio.Threading.Tests/Microsoft.VisualStudio.Threading.Tests.csproj @@ -1,14 +1,17 @@  - net472;netcoreapp3.1;net5.0 + net8.0 + Exe true - true true true $(DefineConstants);ISOLATED_TEST_SUPPORT + + $(TargetFrameworks);net472;net8.0-windows + InternalUtilities.cs @@ -16,21 +19,20 @@ ListOfOftenOne`1.cs + + RarelyRemoveItemSet`1.cs + WeakKeyDictionary`2.cs - - - - - - - - - + + + + + diff --git a/test/Microsoft.VisualStudio.Threading.Tests/NoMessagePumpSyncContextTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/NoMessagePumpSyncContextTests.cs new file mode 100644 index 000000000..245006e50 --- /dev/null +++ b/test/Microsoft.VisualStudio.Threading.Tests/NoMessagePumpSyncContextTests.cs @@ -0,0 +1,230 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Threading; +using System.Threading.Tasks; + +/// +/// Tests for . +/// +public class NoMessagePumpSyncContextTests : TestBase +{ + /// + /// Initializes a new instance of the class. + /// + /// The logger to use for test output. + public NoMessagePumpSyncContextTests(ITestOutputHelper logger) + : base(logger) + { + } + + /// + /// Verifies that returns a usable singleton instance. + /// + [Fact] + public void Default_IsNonNull() + { + Assert.NotNull(NoMessagePumpSyncContext.Default); + } + + /// + /// Verifies that the default singleton is itself a . + /// + [Fact] + public void Default_IsNoMessagePumpSyncContext() + { + Assert.IsType(NoMessagePumpSyncContext.Default); + } + + /// + /// Verifies that schedules work on the thread pool + /// when no underlying sync context is provided. + /// + [Fact] + public async Task Post_DefaultConstructor_ExecutesOnThreadPool() + { + NoMessagePumpSyncContext sc = new(); + TaskCompletionSource tcs = new(); + sc.Post(_ => tcs.SetResult(Thread.CurrentThread.IsThreadPoolThread), null); + Assert.True(await tcs.Task.WithCancellation(this.TimeoutToken)); + } + + /// + /// Verifies that executes work synchronously + /// on the calling thread when no underlying sync context is provided. + /// + [Fact] + public void Send_DefaultConstructor_ExecutesInlineOnCallingThread() + { + NoMessagePumpSyncContext sc = new(); + int callingThreadId = Thread.CurrentThread.ManagedThreadId; + int? callbackThreadId = null; + bool callbackInvoked = false; + + sc.Send( + _ => + { + callbackInvoked = true; + callbackThreadId = Thread.CurrentThread.ManagedThreadId; + }, + null); + + Assert.True(callbackInvoked); + Assert.Equal(callingThreadId, callbackThreadId); + } + + /// + /// Verifies that throws + /// when a null underlying context is passed. + /// + [Fact] + public void Constructor_WithNullUnderlyingContext_Throws() + { + Assert.Throws(() => new NoMessagePumpSyncContext(null!)); + } + + /// + /// Verifies that rejects a null callback before + /// delegating to the underlying sync context. + /// + [Fact] + public void Post_WithNullCallback_Throws() + { + ThrowingSyncContext underlying = new(); + NoMessagePumpSyncContext sc = new(underlying); + + Assert.Throws(() => sc.Post(null!, null)); + Assert.False(underlying.PostInvoked); + } + + /// + /// Verifies that rejects a null callback before + /// delegating to the underlying sync context. + /// + [Fact] + public void Send_WithNullCallback_Throws() + { + ThrowingSyncContext underlying = new(); + NoMessagePumpSyncContext sc = new(underlying); + + Assert.Throws(() => sc.Send(null!, null)); + Assert.False(underlying.SendInvoked); + } + + /// + /// Verifies that delegates to the underlying + /// sync context when one is provided. + /// + [Fact] + public async Task Post_WithUnderlyingContext_DelegatesToUnderlying() + { + TaskCompletionSource tcs = new(); + RecordingPostSyncContext underlying = new(posted: _ => tcs.SetResult(true)); + NoMessagePumpSyncContext sc = new(underlying); + sc.Post(_ => { }, null); + Assert.True(await tcs.Task.WithCancellation(this.TimeoutToken)); + } + + /// + /// Verifies that delegates to the underlying + /// sync context when one is provided. + /// + [Fact] + public void Send_WithUnderlyingContext_DelegatesToUnderlying() + { + bool sendInvoked = false; + RecordingSendSyncContext underlying = new(sent: _ => sendInvoked = true); + NoMessagePumpSyncContext sc = new(underlying); + sc.Send(_ => { }, null); + Assert.True(sendInvoked); + } + +#if NETFRAMEWORK + /// + /// Establishes the baseline: on a plain STA thread without a special synchronization context, + /// uses CoWaitForMultipleHandles, + /// which allows COM RPC calls to be dispatched to the thread while it waits. + /// + [StaFact] + public void Wait_ComRpcPenetratesDefaultStaWait() + { + using CoWaitMainThreadTransition probe = new(); + using ManualResetEvent mre = new(false); + + // Block the STA thread; the default CoWait allows the COM call to execute Signal(). + mre.WaitOne((int)ExpectedTimeout.TotalMilliseconds); + + // The COM call should have been delivered while the thread was blocked. + Assert.True(probe.Wait(TimeSpan.FromMilliseconds(AsyncDelay))); + } + + /// + /// Verifies that uses + /// WaitForMultipleObjects rather than CoWaitForMultipleHandles, preventing + /// COM RPC calls from being dispatched to the thread while it is synchronously waiting. + /// + [StaFact] + public void Wait_BlocksComRpcCalls() + { + using (NoMessagePumpSyncContext.Default.Apply()) + { + using CoWaitMainThreadTransition probe = new(); + using ManualResetEvent mre = new(false); + + // Block the STA thread; NoMessagePumpSyncContext uses WaitForMultipleObjects, + // so the COM call cannot be dispatched while the thread is waiting. + mre.WaitOne((int)ExpectedTimeout.TotalMilliseconds); + + // The COM call should NOT have been delivered. + Assert.False(probe.Wait(TimeSpan.FromMilliseconds(AsyncDelay))); + } + } +#endif + + /// + /// A that invokes a callback when is called. + /// + private class RecordingPostSyncContext(Action posted) : SynchronizationContext + { + public override void Post(SendOrPostCallback d, object? state) + { + posted(d); + base.Post(d, state); + } + } + + /// + /// A that invokes a callback when is called. + /// + private class RecordingSendSyncContext(Action sent) : SynchronizationContext + { + public override void Send(SendOrPostCallback d, object? state) + { + sent(d); + base.Send(d, state); + } + } + + /// + /// A that records whether or were invoked. + /// + private class ThrowingSyncContext : SynchronizationContext + { + public bool PostInvoked { get; private set; } + + public bool SendInvoked { get; private set; } + + public override void Post(SendOrPostCallback d, object? state) + { + this.PostInvoked = true; + throw new InvalidOperationException(); + } + + public override void Send(SendOrPostCallback d, object? state) + { + this.SendInvoked = true; + throw new InvalidOperationException(); + } + } +} diff --git a/test/Microsoft.VisualStudio.Threading.Tests/NonConcurrentSynchronizationContextTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/NonConcurrentSynchronizationContextTests.cs index 57c3748ad..4ac2e9b75 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/NonConcurrentSynchronizationContextTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/NonConcurrentSynchronizationContextTests.cs @@ -1,15 +1,10 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; public class NonConcurrentSynchronizationContextTests : TestBase { @@ -53,7 +48,7 @@ public async Task UnhandledException_WithHandler() var eventArgs = new TaskCompletionSource<(object?, Exception)>(); this.nonSticky.UnhandledException += (s, e) => eventArgs.SetResult((s, e)); this.nonSticky.Post(s => throw new InvalidOperationException(), null); - (object sender, Exception ex) = await eventArgs.Task.WithCancellation(this.TimeoutToken); + (object? sender, Exception? ex) = await eventArgs.Task.WithCancellation(this.TimeoutToken); Assert.Same(this.nonSticky, sender); Assert.IsType(ex); } diff --git a/test/Microsoft.VisualStudio.Threading.Tests/ProgressWithCompletionTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/ProgressWithCompletionTests.cs index 2b1469ed4..41f1c29f3 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/ProgressWithCompletionTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/ProgressWithCompletionTests.cs @@ -1,13 +1,10 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; public class ProgressWithCompletionTests : TestBase { @@ -162,7 +159,7 @@ public void DoesNotDeadlockWhenCallbackCapturesSyncContext(bool captureMainThrea } else { - var progressTask = Task.Run(progressFactory); + Task> progressTask = Task.Run(progressFactory); progressTask.WaitWithoutInlining(); progress = progressTask.Result; } diff --git a/test/Microsoft.VisualStudio.Threading.Tests/RarelyRemoveItemSetTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/RarelyRemoveItemSetTests.cs new file mode 100644 index 000000000..d87d0c071 --- /dev/null +++ b/test/Microsoft.VisualStudio.Threading.Tests/RarelyRemoveItemSetTests.cs @@ -0,0 +1,233 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Linq; +using System.Runtime.CompilerServices; + +public class RarelyRemoveItemSetTests : TestBase +{ + private RarelyRemoveItemSet list; + + public RarelyRemoveItemSetTests(ITestOutputHelper logger) + : base(logger) + { + this.list = default(RarelyRemoveItemSet); + } + + [Fact] + public void EnumerationOfEmpty() + { + using (RarelyRemoveItemSet.Enumerator enumerator = this.list.EnumerateAndClear().GetEnumerator()) + { + Assert.False(enumerator.MoveNext()); + enumerator.Reset(); + Assert.False(enumerator.MoveNext()); + } + } + + [Fact] + public void EnumerationOfOne() + { + this.list.Add(new GenericParameterHelper(1)); + using (RarelyRemoveItemSet.Enumerator enumerator = this.list.EnumerateAndClear().GetEnumerator()) + { + Assert.True(enumerator.MoveNext()); + Assert.Equal(1, enumerator.Current.Data); + Assert.False(enumerator.MoveNext()); + enumerator.Reset(); + Assert.True(enumerator.MoveNext()); + Assert.Equal(1, enumerator.Current.Data); + Assert.False(enumerator.MoveNext()); + } + } + + [Fact] + public void EnumerationOfTwo() + { + this.list.Add(new GenericParameterHelper(1)); + this.list.Add(new GenericParameterHelper(2)); + using (RarelyRemoveItemSet.Enumerator enumerator = this.list.EnumerateAndClear().GetEnumerator()) + { + Assert.True(enumerator.MoveNext()); + Assert.Equal(1, enumerator.Current.Data); + Assert.True(enumerator.MoveNext()); + Assert.Equal(2, enumerator.Current.Data); + Assert.False(enumerator.MoveNext()); + enumerator.Reset(); + Assert.True(enumerator.MoveNext()); + Assert.Equal(1, enumerator.Current.Data); + Assert.True(enumerator.MoveNext()); + Assert.Equal(2, enumerator.Current.Data); + Assert.False(enumerator.MoveNext()); + } + } + + [Fact] + public void RemoveFromEmpty() + { + this.list.Remove(null!); + Assert.Empty(this.list.ToArray()); + this.list.Remove(new GenericParameterHelper(5)); + Assert.Empty(this.list.ToArray()); + } + + [Fact] + public void RemoveFromOne() + { + var value = new GenericParameterHelper(1); + this.list.Add(value); + + this.list.Remove(null!); + Assert.Single(this.list.ToArray()); + this.list.Remove(new GenericParameterHelper(5)); + Assert.Single(this.list.ToArray()); + this.list.Remove(value); + Assert.Empty(this.list.ToArray()); + } + + [Fact] + public void RemoveFromTwoLIFO() + { + var value1 = new GenericParameterHelper(1); + var value2 = new GenericParameterHelper(2); + this.list.Add(value1); + this.list.Add(value2); + + this.list.Remove(null!); + Assert.Equal(2, this.list.ToArray().Length); + this.list.Remove(new GenericParameterHelper(5)); + Assert.Equal(2, this.list.ToArray().Length); + this.list.Remove(value2); + Assert.Single(this.list.ToArray()); + Assert.Equal(1, this.list.ToArray()[0].Data); + this.list.Remove(value1); + Assert.Empty(this.list.ToArray()); + } + + [Fact] + public void RemoveFromTwoFIFO() + { + var value1 = new GenericParameterHelper(1); + var value2 = new GenericParameterHelper(2); + this.list.Add(value1); + this.list.Add(value2); + + this.list.Remove(null!); + Assert.Equal(2, this.list.ToArray().Length); + this.list.Remove(new GenericParameterHelper(5)); + Assert.Equal(2, this.list.ToArray().Length); + this.list.Remove(value1); + Assert.Single(this.list.ToArray()); + Assert.Equal(2, this.list.ToArray()[0].Data); + this.list.Remove(value2); + Assert.Empty(this.list.ToArray()); + } + + [Fact] + public void RemoveFromTwoEnumeration() + { + var value1 = new GenericParameterHelper(1); + var value2 = new GenericParameterHelper(2); + this.list.Add(value1); + this.list.Add(value2); + + this.list.Remove(value1); + this.list.Remove(value2); + + int count = 0; + foreach (GenericParameterHelper item in this.list.EnumerateAndClear()) + { + count++; + } + + Assert.Equal(0, count); + } + + [Fact] + public void RemoveFromMultiple() + { + var values = new GenericParameterHelper[5]; + for (int i = 0; i < 5; i++) + { + values[i] = new GenericParameterHelper(i); + this.list.Add(values[i]); + } + + this.list.Remove(values[2]); + Assert.Equal(4, this.list.ToArray().Length); + AssertContains(0, 1, 3, 4); + + this.list.Remove(values[4]); + Assert.Equal(3, this.list.ToArray().Length); + AssertContains(0, 1, 3); + + this.list.Remove(values[0]); + Assert.Equal(2, this.list.ToArray().Length); + AssertContains(1, 3); + + this.list.Remove(values[3]); + Assert.Single(this.list.ToArray()); + AssertContains(1); + + void AssertContains(params int[] values) + { + // We specifically do not care about order of elements. + Assert.Equal(values.OrderBy(k => k), this.list.ToArray().Select(v => v.Data).OrderBy(k => k)); + } + } + + /// + /// Test to make sure the list does not reference deleted items. + /// + [Fact] + public void RemoveFromMultipleGCTest() + { + WeakReference[]? weakValues = this.RemoveFromMultipleGCTestHelper(); + + GC.Collect(); + + for (int i = 0; i < 5; i++) + { + Assert.False(weakValues[i].IsAlive); + } + } + + [Fact] + public void EnumerateAndClear() + { + this.list.Add(new GenericParameterHelper(1)); + + using (RarelyRemoveItemSet.Enumerator enumerator = this.list.EnumerateAndClear().GetEnumerator()) + { + Assert.Empty(this.list.ToArray()); // The collection should have been cleared. + Assert.True(enumerator.MoveNext()); + Assert.Equal(1, enumerator.Current.Data); + Assert.False(enumerator.MoveNext()); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] // must not be inlined so that locals are guaranteed to be freed. + private WeakReference[] RemoveFromMultipleGCTestHelper() + { + GenericParameterHelper[]? values = new GenericParameterHelper[5]; + var weakValues = new WeakReference[5]; + + for (int i = 0; i < 5; i++) + { + values[i] = new GenericParameterHelper(i); + weakValues[i] = new WeakReference(values[i]); + this.list.Add(values[i]); + } + + this.list.Remove(values[4]); + this.list.Remove(values[1]); + this.list.Remove(values[3]); + this.list.Remove(values[0]); + this.list.Remove(values[2]); + + Assert.Empty(this.list.ToArray()); + + return weakValues; + } +} diff --git a/test/Microsoft.VisualStudio.Threading.Tests/ReentrantSemaphoreJTFTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/ReentrantSemaphoreJTFTests.cs index fe93cd7f9..6c001e1c2 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/ReentrantSemaphoreJTFTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/ReentrantSemaphoreJTFTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -6,9 +6,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; public class ReentrantSemaphoreJTFTests : ReentrantSemaphoreTestBase { diff --git a/test/Microsoft.VisualStudio.Threading.Tests/ReentrantSemaphoreNonJTFTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/ReentrantSemaphoreNonJTFTests.cs index 0c6afa73b..4935e0b64 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/ReentrantSemaphoreNonJTFTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/ReentrantSemaphoreNonJTFTests.cs @@ -1,10 +1,7 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Threading.Tasks; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; public class ReentrantSemaphoreNonJTFTests : ReentrantSemaphoreTestBase { diff --git a/test/Microsoft.VisualStudio.Threading.Tests/ReentrantSemaphoreTestBase.cs b/test/Microsoft.VisualStudio.Threading.Tests/ReentrantSemaphoreTestBase.cs index d5a9482b8..494de17d1 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/ReentrantSemaphoreTestBase.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/ReentrantSemaphoreTestBase.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -6,9 +6,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; public abstract class ReentrantSemaphoreTestBase : TestBase, IDisposable { @@ -341,7 +338,7 @@ await this.semaphore.ExecuteAsync(delegate await this.semaphore.ExecuteAsync(async delegate { releaseInheritor.Set(); - await Assert.ThrowsAnyAsync(() => innerOperation); + await Assert.ThrowsAnyAsync(() => innerOperation!); }); }); @@ -387,7 +384,7 @@ await this.semaphore.ExecuteAsync( async delegate { releaseInheritor2.Set(); - await Assert.ThrowsAnyAsync(() => innerOperation2); + await Assert.ThrowsAnyAsync(() => innerOperation2!); }, this.TimeoutToken); } @@ -540,7 +537,7 @@ public void Stack_ViolationCaughtAtBothSites() // Release the nested one last, which should similarly throw because its parent is already released. release2.Set(); - await Assert.ThrowsAsync(() => operation2); + await Assert.ThrowsAsync(() => operation2!); // Verify that the semaphore is still in a faulted state, and will reject new calls. Assert.Throws(() => this.semaphore.CurrentCount); @@ -695,7 +692,7 @@ await semaphore.ExecuteAsync( await Assert.ThrowsAsync(() => pendingSemaphoreTask).WithCancellation(this.TimeoutToken); releaser1.Set(); - await Assert.ThrowsAsync(() => innerFaulterSemaphoreTask).WithCancellation(this.TimeoutToken); + await Assert.ThrowsAsync(() => innerFaulterSemaphoreTask!).WithCancellation(this.TimeoutToken); await Assert.ThrowsAsync(() => semaphore.ExecuteAsync(() => Task.CompletedTask)).WithCancellation(this.TimeoutToken); }); } diff --git a/test/Microsoft.VisualStudio.Threading.Tests/SingleThreadedSynchronizationContextTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/SingleThreadedSynchronizationContextTests.cs index c6b28ca36..7e9e5ec44 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/SingleThreadedSynchronizationContextTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/SingleThreadedSynchronizationContextTests.cs @@ -1,12 +1,9 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Reflection; using System.Threading.Tasks; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; public class SingleThreadedSynchronizationContextTests : TestBase { @@ -127,7 +124,7 @@ public void Post_DoesNotExecuteSynchronously() [Fact] public void Post_PushFrame() { - var originalThreadId = Environment.CurrentManagedThreadId; + int originalThreadId = Environment.CurrentManagedThreadId; var syncContext = new SingleThreadedSynchronizationContext(); var frame = new SingleThreadedSynchronizationContext.Frame(); @@ -164,7 +161,7 @@ public void Post_PushFrame() [Fact] public void Post_PushFrame_Throws() { - var originalThreadId = Environment.CurrentManagedThreadId; + int originalThreadId = Environment.CurrentManagedThreadId; var syncContext = new SingleThreadedSynchronizationContext(); var frame = new SingleThreadedSynchronizationContext.Frame(); @@ -184,7 +181,7 @@ public void Post_CapturesExecutionContext() { try { - var expectedValue = new object(); + object expectedValue = new object(); var actualValue = new TaskCompletionSource(); var asyncLocal = new AsyncLocal(); diff --git a/test/Microsoft.VisualStudio.Threading.Tests/SingleThreadedTestSynchronizationContext.cs b/test/Microsoft.VisualStudio.Threading.Tests/SingleThreadedTestSynchronizationContext.cs index 029563f99..0bd798d8a 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/SingleThreadedTestSynchronizationContext.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/SingleThreadedTestSynchronizationContext.cs @@ -1,14 +1,13 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -#if NETFRAMEWORK +#if NETFRAMEWORK || WINDOWS #define UseWpfContext #endif using System.Diagnostics.CodeAnalysis; using System.Threading; using Microsoft; -using Microsoft.VisualStudio.Threading; #if UseWpfContext using System.Windows.Threading; #endif diff --git a/test/Microsoft.VisualStudio.Threading.Tests/TestBase.cs b/test/Microsoft.VisualStudio.Threading.Tests/TestBase.cs index 2276833ae..704e1440b 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/TestBase.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/TestBase.cs @@ -1,16 +1,15 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Microsoft; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; +using Xunit.Sdk; public abstract class TestBase { @@ -22,7 +21,7 @@ public abstract class TestBase /// The maximum length of time to wait for something that we expect will happen /// within the timeout. /// - protected static readonly TimeSpan UnexpectedTimeout = Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10); + protected static readonly TimeSpan UnexpectedTimeout = Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(15); /// /// The maximum length of time to wait for something that we do not expect will happen @@ -37,6 +36,13 @@ protected TestBase(ITestOutputHelper logger) this.Logger = logger; } + protected static bool MightCoWaitBeUsed +#if NETFRAMEWORK + => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); +#else + => false; +#endif + /// /// Gets or sets the source of that influences /// when tests consider themselves to be timed out. @@ -130,7 +136,7 @@ protected void CheckGCPressure(Action scenario, int maxBytesAllocated, int itera /// The maximum number of bytes allowed to be allocated by one run of the scenario. Use -1 to indicate no limit. /// The number of times to invoke in a row before measuring average memory impact. /// The number of times the (scenario * iterations) loop repeats with a failing result before ultimately giving up. - /// true to synchronously complete instead of yielding. + /// to synchronously complete instead of yielding. /// A task that captures the result of the operation. protected async Task CheckGCPressureAsync(Func scenario, int maxBytesAllocated, int iterations = 100, int allowedAttempts = GCAllocationAttempts, bool completeSynchronously = false) { @@ -336,10 +342,10 @@ protected void ExecuteOnDispatcher(Func action) /// /// The name of the test method. /// - /// A task whose result is true if test execution is already isolated and should therefore proceed with the body of the test, - /// or false after the isolated instance of the test has completed execution. + /// A task whose result is if test execution is already isolated and should therefore proceed with the body of the test, + /// or after the isolated instance of the test has completed execution. /// - /// Thrown if the isolated test result is a Failure. + /// Thrown if the isolated test result is a Failure. /// Thrown if on a platform that we do not yet support test isolation on. protected Task ExecuteInIsolationAsync([CallerMemberName] string testMethodName = null!) { @@ -352,10 +358,10 @@ protected Task ExecuteInIsolationAsync([CallerMemberName] string testMetho /// /// The name of the test method. /// - /// true if test execution is already isolated and should therefore proceed with the body of the test, - /// or false after the isolated instance of the test has completed execution. + /// if test execution is already isolated and should therefore proceed with the body of the test, + /// or after the isolated instance of the test has completed execution. /// - /// Thrown if the isolated test result is a Failure. + /// Thrown if the isolated test result is a Failure. /// Thrown if on a platform that we do not yet support test isolation on. protected bool ExecuteInIsolation([CallerMemberName] string testMethodName = null!) { diff --git a/test/Microsoft.VisualStudio.Threading.Tests/TestBaseTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/TestBaseTests.cs index bf934f217..26ef0eb7f 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/TestBaseTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/TestBaseTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -6,9 +6,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; using Xunit.Sdk; public class TestBaseTests : TestBase @@ -18,10 +15,10 @@ public TestBaseTests(ITestOutputHelper logger) { } - [SkippableFact] + [Fact] public void ExecuteOnSTA_ExecutesDelegateOnSTA() { - Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Windows only"); bool executed = false; this.ExecuteOnSTA(delegate { @@ -32,10 +29,10 @@ public void ExecuteOnSTA_ExecutesDelegateOnSTA() Assert.True(executed); } - [SkippableFact] + [Fact] public void ExecuteOnSTA_PropagatesExceptions() { - Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Windows only"); Assert.Throws(() => this.ExecuteOnSTA(() => { throw new ApplicationException(); @@ -90,7 +87,8 @@ public void ExecuteOnDispatcher_PropagatesExceptions() })); } - [SkippableFact] + [Fact] + [Trait("TestCategory", "FailsInCloudTest")] public async Task ExecuteInIsolation_PassingTest() { if (await this.ExecuteInIsolationAsync()) @@ -99,7 +97,7 @@ public async Task ExecuteInIsolation_PassingTest() } } - [SkippableFact] + [Fact] public async Task ExecuteInIsolation_FailingTest() { bool executeHere; @@ -120,6 +118,7 @@ public async Task ExecuteInIsolation_FailingTest() #if NETFRAMEWORK [StaFact] + [Trait("TestCategory", "FailsInCloudTest")] public async Task ExecuteInIsolation_PassingOnSTA() { if (await this.ExecuteInIsolationAsync()) diff --git a/test/Microsoft.VisualStudio.Threading.Tests/TestUtilities.cs b/test/Microsoft.VisualStudio.Threading.Tests/TestUtilities.cs index 5363c7da7..1c7872500 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/TestUtilities.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/TestUtilities.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -10,9 +10,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; +using Xunit.Sdk; internal static class TestUtilities { @@ -69,7 +67,7 @@ internal static T[] ConcurrencyTest(Func action, int concurrency = -1) concurrency = Environment.ProcessorCount; } - Skip.If(Environment.ProcessorCount < concurrency, $"The test machine does not have enough CPU cores to exercise a concurrency level of {concurrency}"); + Assert.SkipWhen(Environment.ProcessorCount < concurrency, $"The test machine does not have enough CPU cores to exercise a concurrency level of {concurrency}"); // We use a barrier to guarantee that all threads are fully ready to // execute the provided function at precisely the same time. @@ -94,11 +92,10 @@ internal static T[] ConcurrencyTest(Func action, int concurrency = -1) internal static DebugAssertionRevert DisableAssertionDialog() { #if NETFRAMEWORK - DefaultTraceListener? listener = Debug.Listeners.OfType().FirstOrDefault(); - if (listener is object) - { - listener.AssertUiEnabled = false; - } + Debug.Listeners.OfType().FirstOrDefault()?.AssertUiEnabled = false; + + // Xunit v3 adds a listener that throws on assertions; remove it so we can test actual runtime functionality of the library. + Debug.Listeners.Remove("xUnit.net"); #else Trace.Listeners.Clear(); #endif @@ -167,10 +164,10 @@ internal static IDisposable StarveThreadpool() /// The name of the test method. /// An optional logger to forward any output to from the isolated test runner. /// - /// A task whose result is true if test execution is already isolated and should therefore proceed with the body of the test, - /// or false after the isolated instance of the test has completed execution. + /// A task whose result is if test execution is already isolated and should therefore proceed with the body of the test, + /// or after the isolated instance of the test has completed execution. /// - /// Thrown if the isolated test result is a Failure. + /// Thrown if the isolated test result is a Failure. /// Thrown if on a platform that we do not yet support test isolation on. internal static Task ExecuteInIsolationAsync(object testClass, string testMethodName, ITestOutputHelper logger) { @@ -186,10 +183,10 @@ internal static Task ExecuteInIsolationAsync(object testClass, string test /// The name of the test method. /// An optional logger to forward any output to from the isolated test runner. /// - /// A task whose result is true if test execution is already isolated and should therefore proceed with the body of the test, - /// or false after the isolated instance of the test has completed execution. + /// A task whose result is if test execution is already isolated and should therefore proceed with the body of the test, + /// or after the isolated instance of the test has completed execution. /// - /// Thrown if the isolated test result is a Failure. + /// Thrown if the isolated test result is a Failure. /// Thrown if on a platform that we do not yet support test isolation on. #pragma warning disable CA1801 // Review unused parameters internal static Task ExecuteInIsolationAsync(string testClassName, string testMethodName, ITestOutputHelper logger) @@ -248,7 +245,7 @@ internal static Task ExecuteInIsolationAsync(string testClassName, string switch (t.Result) { case IsolatedTestHost.ExitCode.TestSkipped: - throw new SkipException("Test skipped. See output of isolated task for details."); + throw SkipException.ForSkip("Test skipped. See output of isolated task for details."); case IsolatedTestHost.ExitCode.TestPassed: default: Assert.Equal(IsolatedTestHost.ExitCode.TestPassed, t.Result); @@ -259,7 +256,7 @@ internal static Task ExecuteInIsolationAsync(string testClassName, string }, TaskScheduler.Default); #else - return Task.FromException(new SkipException("Test isolation is not yet supported on this platform.")); + return Task.FromException(SkipException.ForSkip("Test isolation is not yet supported on this platform.")); #endif } @@ -267,8 +264,8 @@ internal static Task ExecuteInIsolationAsync(string testClassName, string /// Wait on a task without possibly inlining it to the current thread. /// /// The task to wait on. - /// true to throw the original (inner) exception when the faults; false to throw . - /// Thrown if completes in a faulted state if is false. + /// to throw the original (inner) exception when the faults; to throw . + /// Thrown if completes in a faulted state if is . internal static void WaitWithoutInlining(this Task task, bool throwOriginalException) { Requires.NotNull(task, nameof(task)); @@ -295,9 +292,9 @@ internal static void WaitWithoutInlining(this Task task, bool throwOriginalExcep /// /// The type of result returned from the . /// The task to wait on. - /// true to throw the original (inner) exception when the faults; false to throw . + /// to throw the original (inner) exception when the faults; to throw . /// The result of the . - /// Thrown if completes in a faulted state if is false. + /// Thrown if completes in a faulted state if is . internal static T GetResultWithoutInlining(this Task task, bool throwOriginalException = true) { WaitWithoutInlining(task, throwOriginalException); diff --git a/test/Microsoft.VisualStudio.Threading.Tests/TestUtilitiesTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/TestUtilitiesTests.cs index b15a45d67..82a530f1d 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/TestUtilitiesTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/TestUtilitiesTests.cs @@ -1,9 +1,7 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Threading.Tasks; -using Microsoft.VisualStudio.Threading; -using Xunit; public class TestUtilitiesTests { diff --git a/test/Microsoft.VisualStudio.Threading.Tests/ThreadingToolsTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/ThreadingToolsTests.cs index 158f9c537..a670da2c9 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/ThreadingToolsTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/ThreadingToolsTests.cs @@ -1,13 +1,10 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; public class ThreadingToolsTests : TestBase { @@ -36,14 +33,14 @@ public void ApplyChangeOptimisticallyWithItem() public void WithCancellationNull() { Assert.Throws(new Action(() => - ThreadingTools.WithCancellation(null!, CancellationToken.None))); + ThreadingTools.WithCancellation(null!, CancellationToken.None).Forget())); } [Fact] public void WithCancellationOfTNull() { Assert.Throws(new Action(() => - ThreadingTools.WithCancellation(null!, CancellationToken.None))); + ThreadingTools.WithCancellation(null!, CancellationToken.None).Forget())); } /// @@ -118,7 +115,7 @@ public void WithCancellationOfPrecanceledTaskOfT() Assert.Same(tcs.Task, tcs.Task.WithCancellation(cts.Token)); } - [SkippableFact] + [Fact] public void WithCancellationAndPrecancelledToken() { var tcs = new TaskCompletionSource(); @@ -179,7 +176,7 @@ public void WithCancellationOfTNoDeadlockFromSyncContext() try { tcs.Task.WithCancellation(cts.Token).Wait(TestTimeout); - Assert.True(false, "Expected OperationCanceledException not thrown."); + Assert.Fail("Expected OperationCanceledException not thrown."); } catch (AggregateException ex) { diff --git a/test/Microsoft.VisualStudio.Threading.Tests/TplExtensionsTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/TplExtensionsTests.cs index 9401e2c37..4d0429637 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/TplExtensionsTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/TplExtensionsTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -6,9 +6,7 @@ using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Sources; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; +using TplExtensions = Microsoft.VisualStudio.Threading.TplExtensions; public class TplExtensionsTests : TestBase { @@ -299,7 +297,7 @@ public void WaitWithoutInlining_Canceled() try { tcs.Task.WaitWithoutInlining(); - Assert.False(true, "Expected exception not thrown."); + Assert.Fail("Expected exception not thrown."); } catch (AggregateException ex) { @@ -430,6 +428,252 @@ public async Task NoThrowAwaitable_UnsafeOnCompleted_DoesNotCaptureExecutionCont await testResultTcs.Task.WithTimeout(UnexpectedTimeout); } + [Fact] + public async Task NoThrowAwaitable_ValueTask() + { + var tcs = new TaskCompletionSource(); + TplExtensions.NoThrowValueTaskAwaitable nothrowTask = new ValueTask(tcs.Task).NoThrowAwaitable(); + Assert.False(nothrowTask.GetAwaiter().IsCompleted); + tcs.SetException(new InvalidOperationException()); + await nothrowTask; + + tcs = new TaskCompletionSource(); + nothrowTask = new ValueTask(tcs.Task).NoThrowAwaitable(); + Assert.False(nothrowTask.GetAwaiter().IsCompleted); + tcs.SetCanceled(); + await nothrowTask; + } + + /// + /// Verifies that independent of whether the or + /// is captured and used to schedule the continuation, the is always captured and applied. + /// + [Theory] + [CombinatorialData] + public async Task NoThrowAwaitable_ValueTask_Await_CapturesExecutionContext(bool captureContext) + { + var awaitableTcs = new TaskCompletionSource(); + var asyncLocal = new System.Threading.AsyncLocal(); + asyncLocal.Value = "expected"; + var testResult = Task.Run(async delegate + { + await new ValueTask(awaitableTcs.Task).NoThrowAwaitable(captureContext); // uses UnsafeOnCompleted + Assert.Equal("expected", asyncLocal.Value); + }); + asyncLocal.Value = null; + await Task.Delay(AsyncDelay); // Make sure the delegate above has time to yield + awaitableTcs.SetResult(null); + + await testResult.WithTimeout(UnexpectedTimeout); + } + + /// + /// Verifies that independent of whether the or + /// is captured and used to schedule the continuation, the is always captured and applied. + /// + [Theory] + [CombinatorialData] + public async Task NoThrowAwaitable_ValueTask_OnCompleted_CapturesExecutionContext(bool captureContext) + { + var testResultTcs = new TaskCompletionSource(); + var awaitableTcs = new TaskCompletionSource(); + var asyncLocal = new System.Threading.AsyncLocal(); + asyncLocal.Value = "expected"; + TplExtensions.NoThrowValueTaskAwaiter awaiter = new ValueTask(awaitableTcs.Task).NoThrowAwaitable(captureContext).GetAwaiter(); + awaiter.OnCompleted(delegate + { + try + { + Assert.Equal("expected", asyncLocal.Value); + testResultTcs.SetResult(null); + } + catch (Exception ex) + { + testResultTcs.SetException(ex); + } + }); + asyncLocal.Value = null; + await Task.Yield(); + awaitableTcs.SetResult(null); + + await testResultTcs.Task.WithTimeout(UnexpectedTimeout); + } + + [Theory] + [CombinatorialData] + public async Task NoThrowAwaitable_ValueTask_UnsafeOnCompleted_DoesNotCaptureExecutionContext(bool captureContext) + { + var testResultTcs = new TaskCompletionSource(); + var awaitableTcs = new TaskCompletionSource(); + var asyncLocal = new System.Threading.AsyncLocal(); + asyncLocal.Value = "expected"; + TplExtensions.NoThrowValueTaskAwaiter awaiter = new ValueTask(awaitableTcs.Task).NoThrowAwaitable(captureContext).GetAwaiter(); + awaiter.UnsafeOnCompleted(delegate + { + try + { + Assert.Null(asyncLocal.Value); + testResultTcs.SetResult(null); + } + catch (Exception ex) + { + testResultTcs.SetException(ex); + } + }); + asyncLocal.Value = null; + await Task.Yield(); + awaitableTcs.SetResult(null); + + await testResultTcs.Task.WithTimeout(UnexpectedTimeout); + } + + [Fact] + public async Task NoThrowAwaitable_ValueTaskT_Succeeds() + { + var barrier = new TaskCompletionSource(); + object result = new object(); + var tcs = new TaskCompletionSource(); + var test = Task.Run(async () => + { + ValueTask awaitable = MethodAsync(barrier, result).Preserve(); + await awaitable.NoThrowAwaitable(); + Assert.True(awaitable.IsCompletedSuccessfully); + Assert.Same(result, awaitable.Result); + }); + + barrier.SetResult(null); + await test; + + static async ValueTask MethodAsync(TaskCompletionSource barrier, object result) + { + await barrier.Task; + return result; + } + } + + [Fact] + public async Task NoThrowAwaitable_ValueTaskT_Fails() + { + var barrier = new TaskCompletionSource(); + var result = new InvalidOperationException(); + var tcs = new TaskCompletionSource(); + var test = Task.Run(async () => + { + ValueTask awaitable = MethodAsync(barrier, result).Preserve(); + await awaitable.NoThrowAwaitable(); + Assert.True(awaitable.IsFaulted); + Assert.Same(result, awaitable.AsTask().Exception!.InnerException); + }); + + barrier.SetResult(null); + await test; + + static async ValueTask MethodAsync(TaskCompletionSource barrier, Exception result) + { + await barrier.Task; + throw result; + } + } + + [Fact] + public async Task NoThrowAwaitable_ValueTaskT() + { + var tcs = new TaskCompletionSource(); + TplExtensions.NoThrowValueTaskAwaitable nothrowTask = new ValueTask(tcs.Task).NoThrowAwaitable(); + Assert.False(nothrowTask.GetAwaiter().IsCompleted); + tcs.SetException(new InvalidOperationException()); + await nothrowTask; + + tcs = new TaskCompletionSource(); + nothrowTask = new ValueTask(tcs.Task).NoThrowAwaitable(); + Assert.False(nothrowTask.GetAwaiter().IsCompleted); + tcs.SetCanceled(); + await nothrowTask; + } + + /// + /// Verifies that independent of whether the or + /// is captured and used to schedule the continuation, the is always captured and applied. + /// + [Theory] + [CombinatorialData] + public async Task NoThrowAwaitable_ValueTaskT_Await_CapturesExecutionContext(bool captureContext) + { + var awaitableTcs = new TaskCompletionSource(); + var asyncLocal = new System.Threading.AsyncLocal(); + asyncLocal.Value = "expected"; + var testResult = Task.Run(async delegate + { + await new ValueTask(awaitableTcs.Task).NoThrowAwaitable(captureContext); // uses UnsafeOnCompleted + Assert.Equal("expected", asyncLocal.Value); + }); + asyncLocal.Value = null; + await Task.Delay(AsyncDelay); // Make sure the delegate above has time to yield + awaitableTcs.SetResult(null); + + await testResult.WithTimeout(UnexpectedTimeout); + } + + /// + /// Verifies that independent of whether the or + /// is captured and used to schedule the continuation, the is always captured and applied. + /// + [Theory] + [CombinatorialData] + public async Task NoThrowAwaitable_ValueTaskT_OnCompleted_CapturesExecutionContext(bool captureContext) + { + var testResultTcs = new TaskCompletionSource(); + var awaitableTcs = new TaskCompletionSource(); + var asyncLocal = new System.Threading.AsyncLocal(); + asyncLocal.Value = "expected"; + TplExtensions.NoThrowValueTaskAwaiter awaiter = new ValueTask(awaitableTcs.Task).NoThrowAwaitable(captureContext).GetAwaiter(); + awaiter.OnCompleted(delegate + { + try + { + Assert.Equal("expected", asyncLocal.Value); + testResultTcs.SetResult(null); + } + catch (Exception ex) + { + testResultTcs.SetException(ex); + } + }); + asyncLocal.Value = null; + await Task.Yield(); + awaitableTcs.SetResult(null); + + await testResultTcs.Task.WithTimeout(UnexpectedTimeout); + } + + [Theory] + [CombinatorialData] + public async Task NoThrowAwaitable_ValueTaskT_UnsafeOnCompleted_DoesNotCaptureExecutionContext(bool captureContext) + { + var testResultTcs = new TaskCompletionSource(); + var awaitableTcs = new TaskCompletionSource(); + var asyncLocal = new System.Threading.AsyncLocal(); + asyncLocal.Value = "expected"; + TplExtensions.NoThrowValueTaskAwaiter awaiter = new ValueTask(awaitableTcs.Task).NoThrowAwaitable(captureContext).GetAwaiter(); + awaiter.UnsafeOnCompleted(delegate + { + try + { + Assert.Null(asyncLocal.Value); + testResultTcs.SetResult(null); + } + catch (Exception ex) + { + testResultTcs.SetException(ex); + } + }); + asyncLocal.Value = null; + await Task.Yield(); + awaitableTcs.SetResult(null); + + await testResultTcs.Task.WithTimeout(UnexpectedTimeout); + } + [Fact] public void InvokeAsyncNullEverything() { @@ -519,7 +763,7 @@ public void InvokeAsyncAggregatesExceptions() try { task.GetAwaiter().GetResult(); - Assert.True(false, "Expected AggregateException not thrown."); + Assert.Fail("Expected AggregateException not thrown."); } catch (AggregateException ex) { @@ -546,7 +790,7 @@ public void InvokeAsyncOfTAggregatesExceptions() try { task.GetAwaiter().GetResult(); - Assert.True(false, "Expected AggregateException not thrown."); + Assert.Fail("Expected AggregateException not thrown."); } catch (AggregateException ex) { @@ -638,7 +882,7 @@ public void FollowCancelableTaskToCompletionEndsInFault() [Fact] public async Task ToApmOfTWithNoTaskState() { - var state = new object(); + object state = new object(); var tcs = new TaskCompletionSource(); IAsyncResult? beginResult = null; @@ -665,7 +909,7 @@ public async Task ToApmOfTWithNoTaskState() [Fact] public async Task ToApmOfTWithMatchingTaskState() { - var state = new object(); + object state = new object(); var tcs = new TaskCompletionSource(state); IAsyncResult? beginResult = null; @@ -692,7 +936,7 @@ public async Task ToApmOfTWithMatchingTaskState() [Fact] public async Task ToApmWithNoTaskState() { - var state = new object(); + object state = new object(); var tcs = new TaskCompletionSource(); IAsyncResult? beginResult = null; @@ -719,7 +963,7 @@ public async Task ToApmWithNoTaskState() [Fact] public async Task ToApmWithMatchingTaskState() { - var state = new object(); + object state = new object(); var tcs = new TaskCompletionSource(state); IAsyncResult? beginResult = null; @@ -830,7 +1074,7 @@ public void WithTimeout_MinusOneMeansInfiniteTimeout(bool generic) { var tcs = new TaskCompletionSource(); Task? timeoutTask = generic - ? TplExtensions.WithTimeout(tcs.Task, TimeSpan.FromMilliseconds(-1)) + ? TplExtensions.WithTimeout(tcs.Task, TimeSpan.FromMilliseconds(-1)) : TplExtensions.WithTimeout((Task)tcs.Task, TimeSpan.FromMilliseconds(-1)); Assert.False(timeoutTask.IsCompleted); await Task.Delay(AsyncDelay / 2); diff --git a/test/Microsoft.VisualStudio.Threading.Tests/Usings.cs b/test/Microsoft.VisualStudio.Threading.Tests/Usings.cs new file mode 100644 index 000000000..f10604229 --- /dev/null +++ b/test/Microsoft.VisualStudio.Threading.Tests/Usings.cs @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +global using Microsoft.VisualStudio.Threading; +global using Xunit; diff --git a/test/Microsoft.VisualStudio.Threading.Tests/ValidityTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/ValidityTests.cs index e917716dc..176e1de45 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/ValidityTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/ValidityTests.cs @@ -1,9 +1,7 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Reflection; -using Microsoft.VisualStudio.Threading; -using Xunit; public class ValidityTests { diff --git a/test/Microsoft.VisualStudio.Threading.Tests/WeakKeyDictionaryTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/WeakKeyDictionaryTests.cs index c70725bdf..c533a47cd 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/WeakKeyDictionaryTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/WeakKeyDictionaryTests.cs @@ -1,13 +1,10 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; -using Microsoft.VisualStudio.Threading; -using Xunit; -using Xunit.Abstractions; /// /// Tests for the weak dictionary class. diff --git a/test/Microsoft.VisualStudio.Threading.Tests/xunit.runner.json b/test/Microsoft.VisualStudio.Threading.Tests/xunit.runner.json new file mode 100644 index 000000000..f841cf908 --- /dev/null +++ b/test/Microsoft.VisualStudio.Threading.Tests/xunit.runner.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "shadowCopy": false, + "methodDisplay": "method" +} diff --git a/test/NativeAOTCompatibility.Test/NativeAOTCompatibility.Test.csproj b/test/NativeAOTCompatibility.Test/NativeAOTCompatibility.Test.csproj new file mode 100644 index 000000000..1c29d7486 --- /dev/null +++ b/test/NativeAOTCompatibility.Test/NativeAOTCompatibility.Test.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + false + false + + + $(TargetFrameworks);net10.0-windows + + + + + + + + + + + diff --git a/test/NativeAOTCompatibility.Test/Program.cs b/test/NativeAOTCompatibility.Test/Program.cs new file mode 100644 index 000000000..0c1d2e38a --- /dev/null +++ b/test/NativeAOTCompatibility.Test/Program.cs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +System.Console.WriteLine("This test is run by \"dotnet publish -c release -r [RID]-x64\" rather than by executing the program."); diff --git a/test/dirs.proj b/test/dirs.proj new file mode 100644 index 000000000..dde4d2640 --- /dev/null +++ b/test/dirs.proj @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/tools/Convert-PDB.ps1 b/tools/Convert-PDB.ps1 new file mode 100644 index 000000000..27346d683 --- /dev/null +++ b/tools/Convert-PDB.ps1 @@ -0,0 +1,50 @@ +<# +.SYNOPSIS + Converts between Windows PDB and Portable PDB formats. +.PARAMETER DllPath + The path to the DLL whose PDB is to be converted. +.PARAMETER PdbPath + The path to the PDB to convert. May be omitted if the DLL was compiled on this machine and the PDB is still at its original path. +.PARAMETER OutputPath + The path of the output PDB to write. +#> +[CmdletBinding()] +Param( + [Parameter(Mandatory = $true, Position = 0)] + [string]$DllPath, + [Parameter()] + [string]$PdbPath, + [Parameter(Mandatory = $true, Position = 1)] + [string]$OutputPath +) + +if ($IsMacOS -or $IsLinux) { + Write-Error "This script only works on Windows" + return +} + +# This package originally comes from the https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json feed. +# Add this feed as an upstream to whatever feed is in nuget.config if this step fails. +$packageID = 'Microsoft.DiaSymReader.Pdb2Pdb' +$packageVersion = '1.1.0-beta2-21101-01' +try { + $pdb2pdbpath = & "$PSScriptRoot/Download-NuGetPackage.ps1" -PackageId $packageID -Version $packageVersion -Source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json +} +catch { + Write-Error "Failed to install $packageID. Consider adding https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json as an upstream to your nuget.config feed." + return +} + +$outputDirectory = Split-Path $OutputPath -Parent +if ($outputDirectory) { + New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null +} + +$toolpath = Join-Path $pdb2pdbpath 'tools\Pdb2Pdb.exe' +$arguments = $DllPath, '/out', $OutputPath, '/nowarn', '0021' +if ($PdbPath) { + $arguments += '/pdb', $PdbPath +} + +Write-Verbose "$toolpath $arguments" +& $toolpath $arguments diff --git a/tools/Download-NuGetPackage.ps1 b/tools/Download-NuGetPackage.ps1 new file mode 100644 index 000000000..5864c6892 --- /dev/null +++ b/tools/Download-NuGetPackage.ps1 @@ -0,0 +1,87 @@ +<# +.SYNOPSIS + Downloads a NuGet package to a local folder using dotnet package download. +.PARAMETER PackageId + The Package ID to download. +.PARAMETER Version + The version of the package to download. If unspecified, the latest version is downloaded. +.PARAMETER Source + An additional package source to search. Used as a fallback alongside the configured feeds. +.PARAMETER OutputDirectory + The directory to download the package to. By default, it uses the obj\tools folder at the root of the repo. +.PARAMETER ConfigFile + The nuget.config file to use. By default, it uses the repo root nuget.config. +.PARAMETER Verbosity + The verbosity level for the download. Defaults to quiet. +.OUTPUTS + System.String. The path to the downloaded package directory. +#> +[CmdletBinding()] +Param( + [Parameter(Position=1,Mandatory=$true)] + [string]$PackageId, + [Parameter()] + [string]$Version, + [Parameter()] + [string]$Source, + [Parameter()] + [string]$OutputDirectory="$PSScriptRoot\..\obj\tools", + [Parameter()] + [string]$ConfigFile="$PSScriptRoot\..\nuget.config", + [Parameter()] + [ValidateSet('quiet','minimal','normal','detailed','diagnostic')] + [string]$Verbosity='quiet' +) + +if (!(Test-Path $OutputDirectory)) { New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null } +$OutputDirectory = (Resolve-Path $OutputDirectory).Path +$ConfigFile = (Resolve-Path $ConfigFile).Path + +$packageIdLower = $PackageId.ToLowerInvariant() +$packageRoot = Join-Path $OutputDirectory $packageIdLower + +if ($Version) { + $predictedPackageDir = Join-Path $packageRoot $Version + if (Test-Path -Path $predictedPackageDir -PathType Container) { + Write-Output (Resolve-Path $predictedPackageDir).Path + return + } +} + +$packageArg = $PackageId +if ($Version) { $packageArg = "$PackageId@$Version" } + +$extraArgs = @() +if ($Source) { $extraArgs += '--source', $Source } +if ($Version -and $Version -match '-') { $extraArgs += '--prerelease' } + +$prevErrorActionPreference = $ErrorActionPreference +$ErrorActionPreference = 'Continue' +$downloadOutput = & dotnet package download $packageArg --configfile $ConfigFile --output $OutputDirectory --verbosity $Verbosity @extraArgs 2>&1 +$downloadExitCode = $LASTEXITCODE +$ErrorActionPreference = $prevErrorActionPreference + +if ($downloadExitCode -ne 0) { + $downloadOutput | Write-Host + throw "Failed to download package $packageArg (exit code $downloadExitCode)." +} + +# Return the path to the downloaded package directory (dotnet package download uses lowercase id) +if ($Version) { + $packageDir = Get-ChildItem -Path $packageRoot -Directory -ErrorAction SilentlyContinue | + Where-Object { $_.Name -ieq $Version } | + Select-Object -First 1 + if ($packageDir) { $packageDir = $packageDir.FullName } +} else { + # When no version is specified, pick the most recently written version directory. + $packageDir = Get-ChildItem -Path $packageRoot -Directory -ErrorAction SilentlyContinue | + Sort-Object -Property LastWriteTimeUtc -Descending | + Select-Object -First 1 + if ($packageDir) { $packageDir = $packageDir.FullName } +} + +if ($packageDir -and (Test-Path $packageDir)) { + Write-Output $packageDir +} else { + throw "Package directory not found after download. PackageId='$PackageId'; Version='$Version'; OutputDirectory='$OutputDirectory'; PackageRoot='$packageRoot'." +} diff --git a/tools/Get-ArtifactsStagingDirectory.ps1 b/tools/Get-ArtifactsStagingDirectory.ps1 new file mode 100644 index 000000000..18967f4c1 --- /dev/null +++ b/tools/Get-ArtifactsStagingDirectory.ps1 @@ -0,0 +1,15 @@ +Param( + [switch]$CleanIfLocal +) +if ($env:BUILD_ARTIFACTSTAGINGDIRECTORY) { + $ArtifactStagingFolder = $env:BUILD_ARTIFACTSTAGINGDIRECTORY +} elseif ($env:RUNNER_TEMP) { + $ArtifactStagingFolder = Join-Path $env:RUNNER_TEMP _artifacts +} else { + $ArtifactStagingFolder = [System.IO.Path]::GetFullPath("$PSScriptRoot/../obj/_artifacts") + if ($CleanIfLocal -and (Test-Path $ArtifactStagingFolder)) { + Remove-Item $ArtifactStagingFolder -Recurse -Force + } +} + +$ArtifactStagingFolder diff --git a/tools/Get-CodeCovTool.ps1 b/tools/Get-CodeCovTool.ps1 new file mode 100644 index 000000000..734ee6079 --- /dev/null +++ b/tools/Get-CodeCovTool.ps1 @@ -0,0 +1,86 @@ +<# +.SYNOPSIS + Downloads the CodeCov.io uploader tool and returns the path to it. +.PARAMETER AllowSkipVerify + Allows skipping signature verification of the downloaded tool if gpg is not installed. +#> +[CmdletBinding()] +Param( + [switch]$AllowSkipVerify +) + +if ($IsMacOS) { + $codeCovUrl = "https://uploader.codecov.io/latest/macos/codecov" + $toolName = 'codecov' +} +elseif ($IsLinux) { + $codeCovUrl = "https://uploader.codecov.io/latest/linux/codecov" + $toolName = 'codecov' +} +else { + $codeCovUrl = "https://uploader.codecov.io/latest/windows/codecov.exe" + $toolName = 'codecov.exe' +} + +$shaSuffix = ".SHA256SUM" +$sigSuffix = $shaSuffix + ".sig" + +Function Get-FileFromWeb([Uri]$Uri, $OutDir) { + $OutFile = Join-Path $OutDir $Uri.Segments[-1] + if (!(Test-Path $OutFile)) { + Write-Verbose "Downloading $Uri..." + if (!(Test-Path $OutDir)) { New-Item -ItemType Directory -Path $OutDir | Out-Null } + try { + (New-Object System.Net.WebClient).DownloadFile($Uri, $OutFile) + } finally { + # This try/finally causes the script to abort + } + } + + $OutFile +} + +$toolsPath = & "$PSScriptRoot\Get-TempToolsPath.ps1" +$binaryToolsPath = Join-Path $toolsPath codecov +$testingPath = Join-Path $binaryToolsPath unverified +$finalToolPath = Join-Path $binaryToolsPath $toolName + +if (!(Test-Path $finalToolPath)) { + if (Test-Path $testingPath) { + Remove-Item -Recurse -Force $testingPath # ensure we download all matching files + } + $tool = Get-FileFromWeb $codeCovUrl $testingPath + $sha = Get-FileFromWeb "$codeCovUrl$shaSuffix" $testingPath + $sig = Get-FileFromWeb "$codeCovUrl$sigSuffix" $testingPath + $key = Get-FileFromWeb https://keybase.io/codecovsecurity/pgp_keys.asc $testingPath + + if ((Get-Command gpg -ErrorAction SilentlyContinue)) { + Write-Host "Importing codecov key" -ForegroundColor Yellow + gpg --import $key + Write-Host "Verifying signature on codecov hash" -ForegroundColor Yellow + gpg --verify $sig $sha + } else { + if ($AllowSkipVerify) { + Write-Warning "gpg not found. Unable to verify hash signature." + } else { + throw "gpg not found. Unable to verify hash signature. Install gpg or add -AllowSkipVerify to override." + } + } + + Write-Host "Verifying hash on downloaded tool" -ForegroundColor Yellow + $actualHash = (Get-FileHash -LiteralPath $tool -Algorithm SHA256).Hash + $expectedHash = (Get-Content $sha).Split()[0] + if ($actualHash -ne $expectedHash) { + # Validation failed. Delete the tool so we can't execute it. + #Remove-Item $codeCovPath + throw "codecov uploader tool failed signature validation." + } + + Copy-Item $tool $finalToolPath + + if ($IsMacOS -or $IsLinux) { + chmod u+x $finalToolPath + } +} + +return $finalToolPath diff --git a/tools/Get-ExternalSymbolFiles.ps1 b/tools/Get-ExternalSymbolFiles.ps1 new file mode 100644 index 000000000..c5dbfd3c3 --- /dev/null +++ b/tools/Get-ExternalSymbolFiles.ps1 @@ -0,0 +1,118 @@ +[CmdletBinding()] +Param ( +) + +# Symbol servers to search for PDBs, in order of priority. +$SymbolServers = @( + 'https://msdl.microsoft.com/download/symbols' + 'https://symbols.nuget.org/download/symbols' +) + +Function Get-SymbolsFromPackage($id, $version) { + $symbolPackagesPath = "$PSScriptRoot/../obj/SymbolsPackages" + New-Item -ItemType Directory -Path $symbolPackagesPath -Force | Out-Null + $packagePath = $null + + # Download the package from configured feeds (failures are non-fatal for symbol collection) + $previousLastExitCode = $global:LASTEXITCODE + try { + $packagePath = & "$PSScriptRoot\Download-NuGetPackage.ps1" -PackageId $id -Version $version -OutputDirectory $symbolPackagesPath -ErrorAction SilentlyContinue + } + catch { + Write-Warning "Failed to download package $id $version from configured feeds. Skipping if not found locally. $($_.Exception.Message)" + } + $global:LASTEXITCODE = $previousLastExitCode + if (!$packagePath -or !(Test-Path -LiteralPath $packagePath)) { + Write-Warning "Package $id $version not found in configured feeds. Skipping." + return + } + + # Download symbols for each binary using dotnet-symbol + $serverArgs = $SymbolServers | ForEach-Object { '--server-path'; $_ } + $binaries = @(Get-ChildItem -Recurse -LiteralPath $packagePath -Include *.dll, *.exe) + if ($binaries) { + $prevErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + & dotnet symbol --symbols @serverArgs @($binaries.FullName) 2>&1 | Out-Null + $ErrorActionPreference = $prevErrorActionPreference + } + + # Output pairs of binary + PDB paths for archival + Get-ChildItem -Recurse -LiteralPath $packagePath -Filter *.pdb | % { + $rootName = Join-Path $_.Directory $_.BaseName + if ($rootName.EndsWith('.ni')) { + $rootName = $rootName.Substring(0, $rootName.Length - 3) + } + + $dllPath = "$rootName.dll" + $exePath = "$rootName.exe" + if (Test-Path $dllPath) { + $BinaryImagePath = $dllPath + } + elseif (Test-Path $exePath) { + $BinaryImagePath = $exePath + } + else { + Write-Warning "`"$_`" found with no matching binary file." + $BinaryImagePath = $null + } + + if ($BinaryImagePath) { + Write-Output $BinaryImagePath + Write-Output $_.FullName + } + } +} + +Function Get-PackageVersions() { + if ($script:PackageVersions) { + return $script:PackageVersions + } + + $propsPath = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\Directory.Packages.props')).Path + $output = & dotnet msbuild $propsPath -nologo -verbosity:quiet -getItem:PackageVersion 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to evaluate package versions from Directory.Packages.props.`n$($output | Out-String)" + return @{} + } + + $jsonText = ($output | Out-String).Trim() + $jsonStart = $jsonText.IndexOf('{') + if ($jsonStart -lt 0) { + Write-Error 'Failed to locate JSON output from `dotnet msbuild -getItem:PackageVersion`.' + return @{} + } + + $packageVersions = @{} + foreach ($item in @((ConvertFrom-Json $jsonText.Substring($jsonStart)).Items.PackageVersion)) { + $packageVersions[$item.Identity] = $item.Version + } + + $script:PackageVersions = $packageVersions + $packageVersions +} + +Function Get-PackageVersion($id) { + $version = (Get-PackageVersions)[$id] + if (!$version) { + Write-Error "No package version found in Directory.Packages.props for the package '$id'" + } + + $version +} + +# All 1st party packages for which symbols packages are expected should be listed here. +# These must all be sourced from nuget.org, as it is the only feed that supports symbol packages. +# We should NOT add 3rd party packages to this list because PDBs may be unsafe for our debuggers to load, +# so we should only archive 1st party symbols. +$1stPartyPackageIds = @() + +$1stPartyPackageIds | % { + $version = Get-PackageVersion $_ + if ($version) { + Write-Verbose "Downloading symbols for package '$_' version '$version'." + Get-SymbolsFromPackage -id $_ -version $version + } else { + Write-Warning "No version found for package '$_'. Skipping symbol download." + } +} diff --git a/tools/Get-LibTemplateBasis.ps1 b/tools/Get-LibTemplateBasis.ps1 new file mode 100644 index 000000000..2181c77b6 --- /dev/null +++ b/tools/Get-LibTemplateBasis.ps1 @@ -0,0 +1,25 @@ +<# +.SYNOPSIS + Returns the name of the well-known branch in the Library.Template repository upon which HEAD is based. +#> +[CmdletBinding(SupportsShouldProcess = $true)] +Param( + [switch]$ErrorIfNotRelated +) + +# This list should be sorted in order of decreasing specificity. +$branchMarkers = @( + @{ commit = 'fd0a7b25ccf030bbd16880cca6efe009d5b1fffc'; branch = 'microbuild' }; + @{ commit = '05f49ce799c1f9cc696d53eea89699d80f59f833'; branch = 'main' }; +) + +foreach ($entry in $branchMarkers) { + if (git rev-list HEAD | Select-String -Pattern $entry.commit) { + return $entry.branch + } +} + +if ($ErrorIfNotRelated) { + Write-Error "Library.Template has not been previously merged with this repo. Please review https://github.com/AArnott/Library.Template/tree/main?tab=readme-ov-file#readme for instructions." + exit 1 +} diff --git a/tools/Get-NuGetTool.ps1 b/tools/Get-NuGetTool.ps1 new file mode 100644 index 000000000..088c5f307 --- /dev/null +++ b/tools/Get-NuGetTool.ps1 @@ -0,0 +1,49 @@ +<# +.SYNOPSIS + Downloads the NuGet.exe tool and returns the path to it. +.PARAMETER NuGetVersion + The version of the NuGet tool to acquire. +#> +Param( + [Parameter()] + [string]$NuGetVersion='7.3.1' +) + +function Test-NuGetExecutableSignature { + Param( + [Parameter(Mandatory=$true)] + [string]$Path + ) + + if (!(Test-Path -LiteralPath $Path)) { + return $false + } + + $signature = Get-AuthenticodeSignature -FilePath $Path + if ($signature.Status -eq [System.Management.Automation.SignatureStatus]::Valid -and + $null -ne $signature.SignerCertificate -and + $signature.SignerCertificate.Subject -like '*CN=Microsoft Corporation*') { + Write-Verbose "NuGet executable signature is valid." + return $true + } + + Write-Verbose "NuGet executable signature is invalid." + return $false +} + +$toolsPath = & "$PSScriptRoot\Get-TempToolsPath.ps1" +$binaryToolsPath = Join-Path $toolsPath $NuGetVersion +if (!(Test-Path $binaryToolsPath)) { $null = mkdir $binaryToolsPath } +$nugetPath = Join-Path $binaryToolsPath nuget.exe + +if (!(Test-Path $nugetPath) -or -not (Test-NuGetExecutableSignature -Path $nugetPath)) { + Write-Host "Downloading nuget.exe $NuGetVersion..." -ForegroundColor Yellow + (New-Object System.Net.WebClient).DownloadFile("https://dist.nuget.org/win-x86-commandline/v$NuGetVersion/NuGet.exe", $nugetPath) + + if (!(Test-NuGetExecutableSignature -Path $nugetPath)) { + Remove-Item $nugetPath -Force -ErrorAction SilentlyContinue + throw "Downloaded nuget.exe $NuGetVersion failed Authenticode signature validation." + } +} + +return (Resolve-Path $nugetPath).Path diff --git a/tools/Get-ProcDump.ps1 b/tools/Get-ProcDump.ps1 new file mode 100644 index 000000000..9d9bcd8d6 --- /dev/null +++ b/tools/Get-ProcDump.ps1 @@ -0,0 +1,5 @@ +<# +.SYNOPSIS +Downloads 32-bit and 64-bit procdump executables and returns the path to where they were installed. +#> +Join-Path (& "$PSScriptRoot\Download-NuGetPackage.ps1" -PackageId procdump -Version 0.0.1) 'bin' diff --git a/tools/Get-SymbolFiles.ps1 b/tools/Get-SymbolFiles.ps1 new file mode 100644 index 000000000..852adf52b --- /dev/null +++ b/tools/Get-SymbolFiles.ps1 @@ -0,0 +1,66 @@ +<# +.SYNOPSIS + Collect the list of PDBs built in this repo. +.PARAMETER Path + The directory to recursively search for PDBs. +.PARAMETER Tests + A switch indicating to find PDBs only for test binaries instead of only for shipping shipping binaries. +#> +[CmdletBinding()] +param ( + [parameter(Mandatory=$true)] + [string]$Path, + [switch]$Tests +) + +$ActivityName = "Collecting symbols from $Path" +Write-Progress -Activity $ActivityName -CurrentOperation "Discovery PDB files" +$PDBs = Get-ChildItem -rec "$Path/*.pdb" + +# Filter PDBs to product OR test related. +$testregex = "unittest|tests|\.test\.|TestHost" + +Write-Progress -Activity $ActivityName -CurrentOperation "De-duplicating symbols" +$PDBsByHash = @{} +$i = 0 +$PDBs |% { + Write-Progress -Activity $ActivityName -CurrentOperation "De-duplicating symbols" -PercentComplete (100 * $i / $PDBs.Length) + $hash = Get-FileHash $_ + $i++ + Add-Member -InputObject $_ -MemberType NoteProperty -Name Hash -Value $hash.Hash + Write-Output $_ +} | Sort-Object CreationTime |% { + # De-dupe based on hash. Prefer the first match so we take the first built copy. + if (-not $PDBsByHash.ContainsKey($_.Hash)) { + $PDBsByHash.Add($_.Hash, $_.FullName) + Write-Output $_ + } +} |? { + if ($Tests) { + $_.FullName -match $testregex + } else { + $_.FullName -notmatch $testregex + } +} |% { + # Collect the DLLs/EXEs as well. + $rootName = Join-Path $_.Directory $_.BaseName + if ($rootName.EndsWith('.ni')) { + $rootName = $rootName.Substring(0, $rootName.Length - 3) + } + + $dllPath = "$rootName.dll" + $exePath = "$rootName.exe" + if (Test-Path $dllPath) { + $BinaryImagePath = $dllPath + } elseif (Test-Path $exePath) { + $BinaryImagePath = $exePath + } else { + Write-Warning "`"$_`" found with no matching binary file." + $BinaryImagePath = $null + } + + if ($BinaryImagePath) { + Write-Output $BinaryImagePath + Write-Output $_.FullName + } +} diff --git a/azure-pipelines/Get-TempToolsPath.ps1 b/tools/Get-TempToolsPath.ps1 similarity index 100% rename from azure-pipelines/Get-TempToolsPath.ps1 rename to tools/Get-TempToolsPath.ps1 diff --git a/tools/GitHubActions.ps1 b/tools/GitHubActions.ps1 new file mode 100644 index 000000000..c732a0f9f --- /dev/null +++ b/tools/GitHubActions.ps1 @@ -0,0 +1,41 @@ +function Add-GitHubActionsEnvVariable { + param( + [string]$Path = $env:GITHUB_ENV, + [Parameter(Mandatory = $true)] + [string]$Name, + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [string]$Value + ) + + if ([string]::IsNullOrWhiteSpace($Path)) { + throw "GitHub Actions GITHUB_ENV file path must not be empty." + } + + if ([string]::IsNullOrWhiteSpace($Name)) { + throw "GitHub Actions environment variable name must not be empty." + } + + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + $delimiter = [guid]::NewGuid().ToString('N') + [System.IO.File]::AppendAllText($Path, "$Name<<$delimiter`n$Value`n$delimiter`n", $utf8NoBom) +} + +function Add-GitHubActionsPath { + param( + [string]$Path = $env:GITHUB_PATH, + [Parameter(Mandatory = $true)] + [string]$Value + ) + + if ([string]::IsNullOrWhiteSpace($Path)) { + throw "GitHub Actions GITHUB_PATH file path must not be empty." + } + + if ([string]::IsNullOrWhiteSpace($Value)) { + throw "GitHub Actions path entry must not be empty." + } + + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::AppendAllText($Path, "$Value`n", $utf8NoBom) +} diff --git a/tools/Install-DotNetSdk.ps1 b/tools/Install-DotNetSdk.ps1 index 3c82ee2b2..bec37c9eb 100644 --- a/tools/Install-DotNetSdk.ps1 +++ b/tools/Install-DotNetSdk.ps1 @@ -3,7 +3,7 @@ <# .SYNOPSIS Installs the .NET SDK specified in the global.json file at the root of this repository, - along with supporting .NET Core runtimes used for testing. + along with supporting .NET runtimes used for testing. .DESCRIPTION This MAY not require elevation, as the SDK and runtimes are installed locally to this repo location, unless `-InstallLocality machine` is specified. @@ -15,63 +15,95 @@ When using 'repo', environment variables are set to cause the locally installed dotnet SDK to be used. Per-repo can lead to file locking issues when dotnet.exe is left running as a build server and can be mitigated by running `dotnet build-server shutdown`. Per-machine requires elevation and will download and install all SDKs and runtimes to machine-wide locations so all applications can find it. +.PARAMETER SdkOnly + Skips installing the runtime. +.PARAMETER IncludeX86 + Installs a x86 SDK and runtimes in addition to the x64 ones. Only supported on Windows. Ignored on others. +.PARAMETER IncludeAspNetCore + Installs the ASP.NET Core runtime along with the .NET runtime. #> [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Medium')] Param ( [ValidateSet('repo','user','machine')] - [string]$InstallLocality='user' + [string]$InstallLocality='user', + [switch]$SdkOnly, + [switch]$IncludeX86, + [switch]$IncludeAspNetCore ) $DotNetInstallScriptRoot = "$PSScriptRoot/../obj/tools" if (!(Test-Path $DotNetInstallScriptRoot)) { New-Item -ItemType Directory -Path $DotNetInstallScriptRoot -WhatIf:$false | Out-Null } $DotNetInstallScriptRoot = Resolve-Path $DotNetInstallScriptRoot -# Look up actual required .NET Core SDK version from global.json -$sdkVersion = & "$PSScriptRoot/../azure-pipelines/variables/DotNetSdkVersion.ps1" +# Look up actual required .NET SDK version from global.json +$sdks = @(New-Object PSObject -Property @{ Version = & "$PSScriptRoot/variables/DotNetSdkVersion.ps1" }) + +# Sometimes a repo requires extra SDKs to be installed (e.g. msbuild.locator scenarios running in tests). +# In such a circumstance, a precise SDK version or a channel can be added as in the example below: +# $sdks += New-Object PSObject -Property @{ Channel = '8.0' } + +If ($IncludeX86 -and ($IsMacOS -or $IsLinux)) { + Write-Verbose "Ignoring -IncludeX86 switch because 32-bit runtimes are only supported on Windows." + $IncludeX86 = $false +} $arch = [System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture if (!$arch) { # Windows Powershell leaves this blank $arch = 'x64' if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { $arch = 'ARM64' } + if (${env:ProgramFiles(Arm)}) { $arch = 'ARM64' } } -# Search for all .NET Core runtime versions referenced from MSBuild projects and arrange to install them. +# Search for all .NET runtime versions referenced from MSBuild projects and arrange to install them. $runtimeVersions = @() $windowsDesktopRuntimeVersions = @() -Get-ChildItem "$PSScriptRoot\..\src\*.*proj","$PSScriptRoot\..\test\*.*proj","$PSScriptRoot\..\Directory.Build.props" -Recurse |% { - $projXml = [xml](Get-Content -Path $_) - $pg = $projXml.Project.PropertyGroup - if ($pg) { - $targetFrameworks = $pg.TargetFramework - if (!$targetFrameworks) { - $targetFrameworks = $pg.TargetFrameworks - if ($targetFrameworks) { - $targetFrameworks = $targetFrameworks -Split ';' +$aspnetRuntimeVersions = @() +if (!$SdkOnly) { + $projFiles = Get-ChildItem "$PSScriptRoot\..\src\*.*proj", "$PSScriptRoot\..\test\*.*proj" -Recurse + $projFiles += Get-ChildItem "$PSScriptRoot\..\src\Directory.Build.props", "$PSScriptRoot\..\test\Directory.Build.props" -Recurse + $projFiles += Get-Item -LiteralPath "$PSScriptRoot\..\Directory.Build.props" + $projFiles | % { + $projXml = [xml](Get-Content -LiteralPath $_) + $pg = $projXml.Project.PropertyGroup + if ($pg) { + $targetFrameworks = @() + $tf = $pg.TargetFramework + $targetFrameworks += $tf + $tfs = $pg.TargetFrameworks + if ($tfs) { + $targetFrameworks = $tfs -Split ';' } } - } - $targetFrameworks |? { $_ -match 'net(?:coreapp)?(\d+\.\d+)' } |% { - $v = $Matches[1] - $runtimeVersions += $v - if ($v -ge '3.0' -and -not ($IsMacOS -or $IsLinux)) { - $windowsDesktopRuntimeVersions += $v + $targetFrameworks |? { $_ -match 'net(?:coreapp)?(\d+\.\d+)' } |% { + $v = $Matches[1] + $runtimeVersions += $v + $aspnetRuntimeVersions += $v + if ($v -ge '3.0' -and -not ($IsMacOS -or $IsLinux)) { + $windowsDesktopRuntimeVersions += $v + } } - } - # Add target frameworks of the form: netXX - $targetFrameworks |? { $_ -match 'net(\d+\.\d+)' } |% { - $v = $Matches[1] - $runtimeVersions += $v - if (-not ($IsMacOS -or $IsLinux)) { - $windowsDesktopRuntimeVersions += $v + # Add target frameworks of the form: netXX + $targetFrameworks |? { $_ -match 'net(\d+\.\d+)' } |% { + $v = $Matches[1] + $runtimeVersions += $v + $aspnetRuntimeVersions += $v + if (-not ($IsMacOS -or $IsLinux)) { + $windowsDesktopRuntimeVersions += $v + } } - } + } +} + +if (!$IncludeAspNetCore) { + $aspnetRuntimeVersions = @() } Function Get-FileFromWeb([Uri]$Uri, $OutDir) { $OutFile = Join-Path $OutDir $Uri.Segments[-1] if (!(Test-Path $OutFile)) { Write-Verbose "Downloading $Uri..." + if (!(Test-Path $OutDir)) { New-Item -ItemType Directory -Path $OutDir | Out-Null } try { (New-Object System.Net.WebClient).DownloadFile($Uri, $OutFile) } finally { @@ -82,35 +114,80 @@ Function Get-FileFromWeb([Uri]$Uri, $OutDir) { $OutFile } -Function Get-InstallerExe($Version, [switch]$Runtime) { - $sdkOrRuntime = 'Sdk' - if ($Runtime) { $sdkOrRuntime = 'Runtime' } - +Function Get-InstallerExe( + $Version, + $Architecture, + [ValidateSet('Sdk','Runtime','WindowsDesktop')] + [string]$sku +) { # Get the latest/actual version for the specified one - if (([Version]$Version).Build -eq -1) { - $versionInfo = -Split (Invoke-WebRequest -Uri "https://dotnetcli.blob.core.windows.net/dotnet/$sdkOrRuntime/$Version/latest.version" -UseBasicParsing) + $TypedVersion = $null + if (![Version]::TryParse($Version, [ref] $TypedVersion)) { + Write-Error "Unable to parse $Version into an a.b.c.d version. This version cannot be installed machine-wide." + exit 1 + } + + if ($TypedVersion.Build -eq -1) { + $versionInfo = -Split (Invoke-WebRequest -Uri "https://builds.dotnet.microsoft.com/dotnet/$sku/$Version/latest.version" -UseBasicParsing) $Version = $versionInfo[-1] } - Get-FileFromWeb -Uri "https://dotnetcli.blob.core.windows.net/dotnet/$sdkOrRuntime/$Version/dotnet-$($sdkOrRuntime.ToLowerInvariant())-$Version-win-$arch.exe" -OutDir "$DotNetInstallScriptRoot" + $majorMinor = "$($TypedVersion.Major).$($TypedVersion.Minor)" + $ReleasesFile = Join-Path $DotNetInstallScriptRoot "$majorMinor\releases.json" + if (!(Test-Path $ReleasesFile)) { + Get-FileFromWeb -Uri "https://builds.dotnet.microsoft.com/dotnet/release-metadata/$majorMinor/releases.json" -OutDir (Split-Path $ReleasesFile) | Out-Null + } + + $releases = Get-Content $ReleasesFile | ConvertFrom-Json + $url = $null + foreach ($release in $releases.releases) { + $filesElement = $null + if ($release.$sku.version -eq $Version) { + $filesElement = $release.$sku.files + } + if (!$filesElement -and ($sku -eq 'sdk') -and $release.sdks) { + foreach ($sdk in $release.sdks) { + if ($sdk.version -eq $Version) { + $filesElement = $sdk.files + break + } + } + } + + if ($filesElement) { + foreach ($file in $filesElement) { + if ($file.rid -eq "win-$Architecture") { + $url = $file.url + Break + } + } + + if ($url) { + Break + } + } + } + + if ($url) { + Get-FileFromWeb -Uri $url -OutDir $DotNetInstallScriptRoot + } else { + throw "Unable to find release of $sku v$Version" + } } -Function Install-DotNet($Version, [switch]$Runtime) { - if ($Runtime) { $sdkSubstring = '' } else { $sdkSubstring = 'SDK ' } - Write-Host "Downloading .NET Core $sdkSubstring$Version..." - $Installer = Get-InstallerExe -Version $Version -Runtime:$Runtime - Write-Host "Installing .NET Core $sdkSubstring$Version..." +Function Install-DotNet($Version, $Architecture, [ValidateSet('Sdk','Runtime','WindowsDesktop','AspNetCore')][string]$sku = 'Sdk') { + Write-Host "Downloading .NET $sku $Version..." + $Installer = Get-InstallerExe -Version $Version -Architecture $Architecture -sku $sku + Write-Host "Installing .NET $sku $Version..." cmd /c start /wait $Installer /install /passive /norestart if ($LASTEXITCODE -eq 3010) { Write-Verbose "Restart required" } elseif ($LASTEXITCODE -ne 0) { - throw "Failure to install .NET Core SDK" + throw "Failure to install .NET SDK" } } -$switches = @( - '-Architecture',$arch -) +$switches = @() $envVars = @{ # For locally installed dotnet, skip first time experience which takes a long time 'DOTNET_SKIP_FIRST_TIME_EXPERIENCE' = 'true'; @@ -121,18 +198,54 @@ if ($InstallLocality -eq 'machine') { $DotNetInstallDir = '/usr/share/dotnet' } else { $restartRequired = $false - if ($PSCmdlet.ShouldProcess(".NET Core SDK $sdkVersion", "Install")) { - Install-DotNet -Version $sdkVersion - $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + $sdks |% { + if ($_.Version) { $version = $_.Version } else { $version = $_.Channel } + if ($PSCmdlet.ShouldProcess(".NET SDK $version ($arch)", "Install")) { + Install-DotNet -Version $version -Architecture $arch + $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + + if ($IncludeX86) { + Install-DotNet -Version $version -Architecture x86 + $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + } + } + } + + $runtimeVersions | Sort-Object | Get-Unique |% { + if ($PSCmdlet.ShouldProcess(".NET runtime $_", "Install")) { + Install-DotNet -Version $_ -sku Runtime -Architecture $arch + $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + + if ($IncludeX86) { + Install-DotNet -Version $_ -sku Runtime -Architecture x86 + $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + } + } } - $runtimeVersions | Get-Unique |% { - if ($PSCmdlet.ShouldProcess(".NET Core runtime $_", "Install")) { - Install-DotNet -Version $_ -Runtime + $windowsDesktopRuntimeVersions | Sort-Object | Get-Unique |% { + if ($PSCmdlet.ShouldProcess(".NET Windows Desktop $_", "Install")) { + Install-DotNet -Version $_ -sku WindowsDesktop -Architecture $arch $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + + if ($IncludeX86) { + Install-DotNet -Version $_ -sku WindowsDesktop -Architecture x86 + $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + } } } + $aspnetRuntimeVersions | Sort-Object | Get-Unique |% { + if ($PSCmdlet.ShouldProcess("ASP.NET Core $_", "Install")) { + Install-DotNet -Version $_ -sku AspNetCore -Architecture $arch + $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + + if ($IncludeX86) { + Install-DotNet -Version $_ -sku AspNetCore -Architecture x86 + $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + } + } + } if ($restartRequired) { Write-Host -ForegroundColor Yellow "System restart required" Exit 3010 @@ -142,33 +255,44 @@ if ($InstallLocality -eq 'machine') { } } elseif ($InstallLocality -eq 'repo') { $DotNetInstallDir = "$DotNetInstallScriptRoot/.dotnet" + $DotNetX86InstallDir = "$DotNetInstallScriptRoot/x86/.dotnet" } elseif ($env:AGENT_TOOLSDIRECTORY) { $DotNetInstallDir = "$env:AGENT_TOOLSDIRECTORY/dotnet" + $DotNetX86InstallDir = "$env:AGENT_TOOLSDIRECTORY/x86/dotnet" } else { $DotNetInstallDir = Join-Path $HOME .dotnet } -Write-Host "Installing .NET Core SDK and runtimes to $DotNetInstallDir" -ForegroundColor Blue - if ($DotNetInstallDir) { - $switches += '-InstallDir',"`"$DotNetInstallDir`"" + if (!(Test-Path $DotNetInstallDir)) { New-Item -ItemType Directory -Path $DotNetInstallDir } + $DotNetInstallDir = Resolve-Path $DotNetInstallDir + Write-Host "Installing .NET SDK and runtimes to $DotNetInstallDir" -ForegroundColor Blue $envVars['DOTNET_MULTILEVEL_LOOKUP'] = '0' $envVars['DOTNET_ROOT'] = $DotNetInstallDir } +if ($IncludeX86) { + if ($DotNetX86InstallDir) { + if (!(Test-Path $DotNetX86InstallDir)) { New-Item -ItemType Directory -Path $DotNetX86InstallDir } + $DotNetX86InstallDir = Resolve-Path $DotNetX86InstallDir + Write-Host "Installing x86 .NET SDK and runtimes to $DotNetX86InstallDir" -ForegroundColor Blue + } else { + # Only machine-wide or repo-wide installations can handle two unique dotnet.exe architectures. + Write-Error "The installation location or OS isn't supported for x86 installation. Try a different -InstallLocality value." + return 1 + } +} + if ($IsMacOS -or $IsLinux) { - $DownloadUri = "https://raw.githubusercontent.com/dotnet/install-scripts/781752509a890ca7520f1182e8bae71f9a53d754/src/dotnet-install.sh" - $DotNetInstallScriptPath = "$DotNetInstallScriptRoot/dotnet-install.sh" + $DotNetInstallScriptPath = "$PSScriptRoot/dotnet-install.sh" } else { - $DownloadUri = "https://raw.githubusercontent.com/dotnet/install-scripts/781752509a890ca7520f1182e8bae71f9a53d754/src/dotnet-install.ps1" - $DotNetInstallScriptPath = "$DotNetInstallScriptRoot/dotnet-install.ps1" + $DotNetInstallScriptPath = "$PSScriptRoot/dotnet-install.ps1" } +# Verify the cached script exists if (-not (Test-Path $DotNetInstallScriptPath)) { - Invoke-WebRequest -Uri $DownloadUri -OutFile $DotNetInstallScriptPath -UseBasicParsing - if ($IsMacOS -or $IsLinux) { - chmod +x $DotNetInstallScriptPath - } + Write-Error "Cached dotnet-install script not found at $DotNetInstallScriptPath. Run tools/Update-DotNetInstallScript.ps1 to download it." + exit 1 } # In case the script we invoke is in a directory with spaces, wrap it with single quotes. @@ -179,47 +303,123 @@ $DotNetInstallScriptPathExpression = "& '$DotNetInstallScriptPathExpression'" $anythingInstalled = $false $global:LASTEXITCODE = 0 -if ($PSCmdlet.ShouldProcess(".NET Core SDK $sdkVersion", "Install")) { - $anythingInstalled = $true - Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Version $sdkVersion $switches" +$sdks |% { + if ($_.Version) { $parameters = '-Version', $_.Version } else { $parameters = '-Channel', $_.Channel } + + if ($PSCmdlet.ShouldProcess(".NET SDK $_ ($arch)", "Install")) { + $anythingInstalled = $true + Invoke-Expression -Command "$DotNetInstallScriptPathExpression $parameters -Architecture $arch -InstallDir $DotNetInstallDir $switches" + + if ($LASTEXITCODE -ne 0) { + Write-Error ".NET SDK installation failure: $LASTEXITCODE" + exit $LASTEXITCODE + } + } else { + Invoke-Expression -Command "$DotNetInstallScriptPathExpression $parameters -Architecture $arch -InstallDir $DotNetInstallDir $switches -DryRun" + } + + if ($IncludeX86) { + if ($PSCmdlet.ShouldProcess(".NET x86 SDK $_", "Install")) { + $anythingInstalled = $true + Invoke-Expression -Command "$DotNetInstallScriptPathExpression $parameters -Architecture x86 -InstallDir $DotNetX86InstallDir $switches" - if ($LASTEXITCODE -ne 0) { - Write-Error ".NET SDK installation failure: $LASTEXITCODE" - exit $LASTEXITCODE + if ($LASTEXITCODE -ne 0) { + Write-Error ".NET x86 SDK installation failure: $LASTEXITCODE" + exit $LASTEXITCODE + } + } else { + Invoke-Expression -Command "$DotNetInstallScriptPathExpression $parameters -Architecture x86 -InstallDir $DotNetX86InstallDir $switches -DryRun" + } } -} else { - Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Version $sdkVersion $switches -DryRun" } $dotnetRuntimeSwitches = $switches + '-Runtime','dotnet' $runtimeVersions | Sort-Object -Unique |% { - if ($PSCmdlet.ShouldProcess(".NET Core runtime $_", "Install")) { + if ($PSCmdlet.ShouldProcess(".NET $Arch runtime $_", "Install")) { $anythingInstalled = $true - Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ $dotnetRuntimeSwitches" + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $dotnetRuntimeSwitches" if ($LASTEXITCODE -ne 0) { Write-Error ".NET SDK installation failure: $LASTEXITCODE" exit $LASTEXITCODE } } else { - Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ $dotnetRuntimeSwitches -DryRun" + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $dotnetRuntimeSwitches -DryRun" + } + + if ($IncludeX86) { + if ($PSCmdlet.ShouldProcess(".NET x86 runtime $_", "Install")) { + $anythingInstalled = $true + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $dotnetRuntimeSwitches" + + if ($LASTEXITCODE -ne 0) { + Write-Error ".NET SDK installation failure: $LASTEXITCODE" + exit $LASTEXITCODE + } + } else { + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $dotnetRuntimeSwitches -DryRun" + } } } $windowsDesktopRuntimeSwitches = $switches + '-Runtime','windowsdesktop' $windowsDesktopRuntimeVersions | Sort-Object -Unique |% { - if ($PSCmdlet.ShouldProcess(".NET Core WindowsDesktop runtime $_", "Install")) { + if ($PSCmdlet.ShouldProcess(".NET WindowsDesktop $arch runtime $_", "Install")) { $anythingInstalled = $true - Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ $windowsDesktopRuntimeSwitches" + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $windowsDesktopRuntimeSwitches" if ($LASTEXITCODE -ne 0) { Write-Error ".NET SDK installation failure: $LASTEXITCODE" exit $LASTEXITCODE } } else { - Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ $windowsDesktopRuntimeSwitches -DryRun" + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $windowsDesktopRuntimeSwitches -DryRun" + } + + if ($IncludeX86) { + if ($PSCmdlet.ShouldProcess(".NET WindowsDesktop x86 runtime $_", "Install")) { + $anythingInstalled = $true + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $windowsDesktopRuntimeSwitches" + + if ($LASTEXITCODE -ne 0) { + Write-Error ".NET SDK installation failure: $LASTEXITCODE" + exit $LASTEXITCODE + } + } else { + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $windowsDesktopRuntimeSwitches -DryRun" + } + } +} + +$aspnetRuntimeSwitches = $switches + '-Runtime','aspnetcore' + +$aspnetRuntimeVersions | Sort-Object -Unique |% { + if ($PSCmdlet.ShouldProcess(".NET ASP.NET Core $arch runtime $_", "Install")) { + $anythingInstalled = $true + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $aspnetRuntimeSwitches" + + if ($LASTEXITCODE -ne 0) { + Write-Error ".NET SDK installation failure: $LASTEXITCODE" + exit $LASTEXITCODE + } + } else { + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $aspnetRuntimeSwitches -DryRun" + } + + if ($IncludeX86) { + if ($PSCmdlet.ShouldProcess(".NET ASP.NET Core x86 runtime $_", "Install")) { + $anythingInstalled = $true + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $aspnetRuntimeSwitches" + + if ($LASTEXITCODE -ne 0) { + Write-Error ".NET SDK installation failure: $LASTEXITCODE" + exit $LASTEXITCODE + } + } else { + Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $aspnetRuntimeSwitches -DryRun" + } } } @@ -228,5 +428,5 @@ if ($PSCmdlet.ShouldProcess("Set DOTNET environment variables to discover these } if ($anythingInstalled -and ($InstallLocality -ne 'machine') -and !$env:TF_BUILD -and !$env:GITHUB_ACTIONS) { - Write-Warning ".NET Core runtimes or SDKs were installed to a non-machine location. Perform your builds or open Visual Studio from this same environment in order for tools to discover the location of these dependencies." + Write-Warning ".NET runtimes or SDKs were installed to a non-machine location. Perform your builds or open Visual Studio from this same environment in order for tools to discover the location of these dependencies." } diff --git a/tools/Install-NuGetCredProvider.ps1 b/tools/Install-NuGetCredProvider.ps1 index 6d3100349..b776f56d9 100644 --- a/tools/Install-NuGetCredProvider.ps1 +++ b/tools/Install-NuGetCredProvider.ps1 @@ -21,7 +21,7 @@ Param ( $envVars = @{} -$toolsPath = & "$PSScriptRoot\..\azure-pipelines\Get-TempToolsPath.ps1" +$toolsPath = & "$PSScriptRoot\Get-TempToolsPath.ps1" if ($IsMacOS -or $IsLinux) { $installerScript = "installcredprovider.sh" @@ -33,7 +33,7 @@ if ($IsMacOS -or $IsLinux) { $installerScript = Join-Path $toolsPath $installerScript -if (!(Test-Path $installerScript)) { +if (!(Test-Path $installerScript) -or $Force) { Invoke-WebRequest $sourceUrl -OutFile $installerScript } @@ -43,14 +43,14 @@ if ($IsMacOS -or $IsLinux) { chmod u+x $installerScript } -& $installerScript -Force:$Force +& $installerScript -Force:$Force -AddNetfx -InstallNet8 if ($AccessToken) { $endpoints = @() $endpointURIs = @() Get-ChildItem "$PSScriptRoot\..\nuget.config" -Recurse |% { - $nugetConfig = [xml](Get-Content -Path $_) + $nugetConfig = [xml](Get-Content -LiteralPath $_) $nugetConfig.configuration.packageSources.add |? { ($_.value -match '^https://pkgs\.dev\.azure\.com/') -or ($_.value -match '^https://[\w\-]+\.pkgs\.visualstudio\.com/') } |% { if ($endpointURIs -notcontains $_.Value) { diff --git a/tools/Install-NuGetPackage.ps1 b/tools/Install-NuGetPackage.ps1 new file mode 100644 index 000000000..30e9c4fae --- /dev/null +++ b/tools/Install-NuGetPackage.ps1 @@ -0,0 +1,85 @@ +<# +.SYNOPSIS + Installs a NuGet package. +.PARAMETER PackageID + The Package ID to install. +.PARAMETER Version + The version of the package to install. If unspecified, the latest stable release is installed. +.PARAMETER Source + The package source feed to find the package to install from. +.PARAMETER Prerelease + Include prerelease packages when searching for the latest version. +.PARAMETER ExcludeVersion + Installs the package without adding the version to the folder name. +.PARAMETER DirectDownload + Bypass the local cache when downloading packages. +.PARAMETER PackagesDir + The directory to install the package to. By default, it uses the Packages folder at the root of the repo. +.PARAMETER ConfigFile + The nuget.config file to use. By default, it uses :/nuget.config. +.OUTPUTS + System.String. The path to the installed package. +#> +[CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Low')] +Param( + [Parameter(Position=1,Mandatory=$true)] + [string]$PackageId, + [Parameter()] + [string]$Version, + [Parameter()] + [string]$Source, + [Parameter()] + [switch]$Prerelease, + [Parameter()] + [switch]$ExcludeVersion, + [Parameter()] + [switch]$DirectDownload, + [Parameter()] + [string]$PackagesDir="$PSScriptRoot\..\packages", + [Parameter()] + [string]$ConfigFile="$PSScriptRoot\..\nuget.config", + [Parameter()] + [ValidateSet('Quiet','Normal','Detailed')] + [string]$Verbosity='normal' +) + +$nugetPath = & "$PSScriptRoot\Get-NuGetTool.ps1" + +Write-Verbose "Installing $PackageId..." +$nugetArgs = "Install",$PackageId,"-OutputDirectory",$PackagesDir,'-ConfigFile',$ConfigFile +if ($Version) { $nugetArgs += "-Version",$Version } +if ($Source) { $nugetArgs += "-FallbackSource",$Source } +if ($Prerelease) { $nugetArgs += "-Prerelease" } +if ($ExcludeVersion) { $nugetArgs += '-ExcludeVersion' } +if ($DirectDownload) { $nugetArgs += '-DirectDownload' } +$nugetArgs += '-Verbosity',$Verbosity + +if ($PSCmdlet.ShouldProcess($PackageId, 'nuget install')) { + $p = Start-Process $nugetPath $nugetArgs -NoNewWindow -Wait -PassThru + if ($null -ne $p.ExitCode -and $p.ExitCode -ne 0) { + throw "NuGet install failed for package '$PackageId' (version '$Version') with exit code $($p.ExitCode)." + } +} + +# Provide the path to the installed package directory to our caller. +if ($ExcludeVersion) { + $packageDir = Get-ChildItem -Path $PackagesDir -Directory -ErrorAction SilentlyContinue | + Where-Object { $_.Name -ieq $PackageId } | + Select-Object -First 1 +} elseif ($Version) { + $expectedDirectoryName = "$PackageId.$Version" + $packageDir = Get-ChildItem -Path $PackagesDir -Directory -ErrorAction SilentlyContinue | + Where-Object { $_.Name -ieq $expectedDirectoryName } | + Select-Object -First 1 +} else { + $packageDir = Get-ChildItem -Path $PackagesDir -Directory -ErrorAction SilentlyContinue | + Where-Object { $_.Name -like "$PackageId.*" } | + Sort-Object -Property LastWriteTimeUtc -Descending | + Select-Object -First 1 +} + +if ($packageDir -and (Test-Path -Path $packageDir.FullName -PathType Container)) { + Write-Output $packageDir.FullName +} else { + throw "Installed package directory not found. PackageId='$PackageId'; Version='$Version'; ExcludeVersion='$ExcludeVersion'; PackagesDir='$PackagesDir'." +} diff --git a/tools/MergeFrom-Template.ps1 b/tools/MergeFrom-Template.ps1 new file mode 100644 index 000000000..240a57097 --- /dev/null +++ b/tools/MergeFrom-Template.ps1 @@ -0,0 +1,79 @@ + +<# +.SYNOPSIS + Merges the latest changes from Library.Template into HEAD of this repo. +.PARAMETER LocalBranch + The name of the local branch to create at HEAD and use to merge into from Library.Template. +#> +[CmdletBinding(SupportsShouldProcess = $true)] +Param( + [string]$LocalBranch = "dev/$($env:USERNAME)/libtemplateUpdate" +) + +Function Spawn-Tool($command, $commandArgs, $workingDirectory, $allowFailures) { + if ($workingDirectory) { + Push-Location $workingDirectory + } + try { + if ($env:TF_BUILD) { + Write-Host "$pwd >" + Write-Host "##[command]$command $commandArgs" + } + else { + Write-Host "$command $commandArgs" -ForegroundColor Yellow + } + if ($commandArgs) { + & $command @commandArgs + } else { + Invoke-Expression $command + } + if ((!$allowFailures) -and ($LASTEXITCODE -ne 0)) { exit $LASTEXITCODE } + } + finally { + if ($workingDirectory) { + Pop-Location + } + } +} + +$remoteBranch = & $PSScriptRoot\Get-LibTemplateBasis.ps1 -ErrorIfNotRelated +if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE +} + +$LibTemplateUrl = 'https://github.com/aarnott/Library.Template' +Spawn-Tool 'git' ('fetch', $LibTemplateUrl, $remoteBranch) +$SourceCommit = Spawn-Tool 'git' ('rev-parse', 'FETCH_HEAD') +$BaseBranch = Spawn-Tool 'git' ('branch', '--show-current') +$SourceCommitUrl = "$LibTemplateUrl/commit/$SourceCommit" + +# To reduce the odds of merge conflicts at this stage, we always move HEAD to the last successful merge. +$basis = Spawn-Tool 'git' ('rev-parse', 'HEAD') # TODO: consider improving this later + +Write-Host "Merging the $remoteBranch branch of Library.Template ($SourceCommit) into local repo $basis" -ForegroundColor Green + +Spawn-Tool 'git' ('checkout', '-b', $LocalBranch, $basis) $null $true +if ($LASTEXITCODE -eq 128) { + Spawn-Tool 'git' ('checkout', $LocalBranch) + Spawn-Tool 'git' ('merge', $basis) +} + +Spawn-Tool 'git' ('merge', 'FETCH_HEAD', '--no-ff', '-m', "Merge the $remoteBranch branch from $LibTemplateUrl`n`nSpecifically, this merges [$SourceCommit from that repo]($SourceCommitUrl).") +if ($LASTEXITCODE -eq 1) { + Write-Error "Merge conflict detected. Manual resolution required." + exit 1 +} +elseif ($LASTEXITCODE -ne 0) { + Write-Error "Merge failed with exit code $LASTEXITCODE." + exit $LASTEXITCODE +} + +$result = New-Object PSObject -Property @{ + BaseBranch = $BaseBranch # The original branch that was checked out when the script ran. + LocalBranch = $LocalBranch # The name of the local branch that was created before the merge. + SourceCommit = $SourceCommit # The commit from Library.Template that was merged in. + SourceBranch = $remoteBranch # The branch from Library.Template that was merged in. +} + +Write-Host $result +Write-Output $result diff --git a/tools/Prepare-Legacy-Symbols.ps1 b/tools/Prepare-Legacy-Symbols.ps1 new file mode 100644 index 000000000..8a007c2a3 --- /dev/null +++ b/tools/Prepare-Legacy-Symbols.ps1 @@ -0,0 +1,49 @@ +Param( + [string]$Path +) + +$ArtifactStagingFolder = & "$PSScriptRoot/Get-ArtifactsStagingDirectory.ps1" +$ArtifactStagingFolder += '/symbols-legacy' +robocopy $Path $ArtifactStagingFolder /mir /njh /njs /ndl /nfl +$WindowsPdbSubDirName = 'symstore' + +Get-ChildItem "$ArtifactStagingFolder\*.pdb" -Recurse |% { + $dllPath = "$($_.Directory)/$($_.BaseName).dll" + $exePath = "$($_.Directory)/$($_.BaseName).exe" + if (Test-Path $dllPath) { + $BinaryImagePath = $dllPath + } elseif (Test-Path $exePath) { + $BinaryImagePath = $exePath + } else { + Write-Warning "`"$_`" found with no matching binary file." + $BinaryImagePath = $null + } + + if ($BinaryImagePath) { + # Native binaries can't have their PDBs converted to legacy (Windows) format so just skip them + try { + [System.Reflection.AssemblyName]::GetAssemblyName($BinaryImagePath) | Out-Null + $isManaged = $true + } + catch { + $isManaged = $false + } + + if (-not $isManaged) { + Write-Host "Skipping native binary PDB: $_" -ForegroundColor DarkYellow + continue + } + + # Convert the PDB to legacy Windows PDBs + Write-Host "Converting PDB for $_" -ForegroundColor DarkGray + $WindowsPdbDir = "$($_.Directory.FullName)\$WindowsPdbSubDirName" + if (!(Test-Path $WindowsPdbDir)) { mkdir $WindowsPdbDir | Out-Null } + $legacyPdbPath = "$WindowsPdbDir\$($_.BaseName).pdb" + & "$PSScriptRoot\Convert-PDB.ps1" -DllPath $BinaryImagePath -PdbPath $_ -OutputPath $legacyPdbPath + if ($LASTEXITCODE -ne 0) { + Write-Warning "PDB conversion of `"$_`" failed." + } + + Move-Item $legacyPdbPath $_ -Force + } +} diff --git a/tools/Set-EnvVars.ps1 b/tools/Set-EnvVars.ps1 index 3f6f86ba5..c85d6941a 100644 --- a/tools/Set-EnvVars.ps1 +++ b/tools/Set-EnvVars.ps1 @@ -12,13 +12,17 @@ The CmdEnvScriptPath environment variable may be optionally set to a path to a cmd shell script to be created (or appended to if it already exists) that will set the environment variables in cmd.exe that are set within the PowerShell environment. This is used by init.cmd in order to reapply any new environment variables to the parent cmd.exe process that were set in the powershell child process. #> -[CmdletBinding(SupportsShouldProcess=$true)] +[CmdletBinding(SupportsShouldProcess = $true)] Param( - [Parameter(Mandatory=$true, Position=1)] + [Parameter(Mandatory = $true, Position = 1)] $Variables, [string[]]$PrependPath ) +if ($env:GITHUB_ACTIONS) { + . "$PSScriptRoot\GitHubActions.ps1" +} + if ($Variables.Count -eq 0) { return $true } @@ -27,7 +31,8 @@ $cmdInstructions = !$env:TF_BUILD -and !$env:GITHUB_ACTIONS -and !$env:CmdEnvScr if ($cmdInstructions) { Write-Warning "Environment variables have been set that will be lost because you're running under cmd.exe" Write-Host "Environment variables that must be set manually:" -ForegroundColor Blue -} else { +} +else { Write-Host "Environment variables set:" -ForegroundColor Blue Write-Host ($Variables | Out-String) if ($PrependPath) { @@ -44,15 +49,15 @@ if ($env:GITHUB_ACTIONS) { } $CmdEnvScript = '' -$Variables.GetEnumerator() |% { - Set-Item -Path env:$($_.Key) -Value $_.Value +$Variables.GetEnumerator() | % { + Set-Item -LiteralPath env:$($_.Key) -Value $_.Value # If we're running in a cloud CI, set these environment variables so they propagate. if ($env:TF_BUILD) { Write-Host "##vso[task.setvariable variable=$($_.Key);]$($_.Value)" } if ($env:GITHUB_ACTIONS) { - Add-Content -Path $env:GITHUB_ENV -Value "$($_.Key)=$($_.Value)" + Add-GitHubActionsEnvVariable -Name $_.Key -Value ([string]$_.Value) } if ($cmdInstructions) { @@ -68,9 +73,9 @@ if ($IsMacOS -or $IsLinux) { } if ($PrependPath) { - $PrependPath |% { + $PrependPath | % { $newPathValue = "$_$pathDelimiter$env:PATH" - Set-Item -Path env:PATH -Value $newPathValue + Set-Item -LiteralPath env:PATH -Value $newPathValue if ($cmdInstructions) { Write-Host "SET PATH=$newPathValue" } @@ -79,7 +84,7 @@ if ($PrependPath) { Write-Host "##vso[task.prependpath]$_" } if ($env:GITHUB_ACTIONS) { - Add-Content -Path $env:GITHUB_PATH -Value $_ + Add-GitHubActionsPath -Value $_ } $CmdEnvScript += "SET PATH=$_$pathDelimiter%PATH%" @@ -88,10 +93,10 @@ if ($PrependPath) { if ($env:CmdEnvScriptPath) { if (Test-Path $env:CmdEnvScriptPath) { - $CmdEnvScript = (Get-Content -Path $env:CmdEnvScriptPath) + $CmdEnvScript + $CmdEnvScript = (Get-Content -LiteralPath $env:CmdEnvScriptPath) + $CmdEnvScript } - Set-Content -Path $env:CmdEnvScriptPath -Value $CmdEnvScript + Set-Content -LiteralPath $env:CmdEnvScriptPath -Value $CmdEnvScript } return !$cmdInstructions diff --git a/tools/Update-DotNetInstallScript.ps1 b/tools/Update-DotNetInstallScript.ps1 new file mode 100644 index 000000000..bc0cba65b --- /dev/null +++ b/tools/Update-DotNetInstallScript.ps1 @@ -0,0 +1,36 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Updates the cached dotnet-install scripts from the dotnet/install-scripts GitHub repository. +.DESCRIPTION + Downloads the latest dotnet-install.ps1 and dotnet-install.sh scripts from + https://github.com/dotnet/install-scripts and caches them locally to avoid GitHub API rate limiting. + Run this script periodically to get the latest installation scripts. +#> +[CmdletBinding(SupportsShouldProcess = $true)] +Param() + +$ScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path +$DownloadBaseUri = "https://raw.githubusercontent.com/dotnet/install-scripts/main/src" + +$scripts = @('dotnet-install.ps1', 'dotnet-install.sh') + +foreach ($script in $scripts) { + $Uri = "$DownloadBaseUri/$script" + $OutFile = Join-Path $ScriptRoot $script + + Write-Host "Updating $script from GitHub..." + try { + if ($PSCmdlet.ShouldProcess($OutFile, "Update from $Uri")) { + Invoke-WebRequest -Uri $Uri -OutFile $OutFile -UseBasicParsing + Write-Host "✓ Successfully updated $script" -ForegroundColor Green + } + } + catch { + Write-Error "Failed to update ${script}: $_" + exit 1 + } +} + +Write-Host "All cached scripts have been updated." -ForegroundColor Green diff --git a/tools/artifacts/APIScanInputs.ps1 b/tools/artifacts/APIScanInputs.ps1 new file mode 100644 index 000000000..b1550bfa2 --- /dev/null +++ b/tools/artifacts/APIScanInputs.ps1 @@ -0,0 +1,22 @@ +$inputs = & "$PSScriptRoot/symbols.ps1" + +if (!$inputs) { return } + +# Filter out specific files that target OS's that are not subject to APIScan. +# Files that are subject but are not supported must be scanned and an SEL exception filed. +$outputs = @{} +$forbiddenSubPaths = @( + , 'linux-*' + , 'osx*' +) + +$inputs.GetEnumerator() | % { + $list = $_.Value | ? { + $path = $_.Replace('\', '/') + return !($forbiddenSubPaths | ? { $path -like "*/$_/*" }) + } + $outputs[$_.Key] = $list +} + + +$outputs diff --git a/azure-pipelines/artifacts/LocBin.ps1 b/tools/artifacts/LocBin.ps1 similarity index 93% rename from azure-pipelines/artifacts/LocBin.ps1 rename to tools/artifacts/LocBin.ps1 index 85bf5c7bf..3b6945f71 100644 --- a/azure-pipelines/artifacts/LocBin.ps1 +++ b/tools/artifacts/LocBin.ps1 @@ -1,5 +1,7 @@ # Identify LCE files and the binary files they describe $BinRoot = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..\bin") +if (!(Test-Path $BinRoot)) { return } + $FilesToCopy = @() $FilesToCopy += Get-ChildItem -Recurse -File -Path $BinRoot |? { $_.FullName -match '\\Localize\\' } diff --git a/tools/artifacts/VSInsertion.ps1 b/tools/artifacts/VSInsertion.ps1 new file mode 100644 index 000000000..32d35dff3 --- /dev/null +++ b/tools/artifacts/VSInsertion.ps1 @@ -0,0 +1,48 @@ +# This artifact captures everything needed to insert into VS (NuGet packages, insertion metadata, etc.) + +[CmdletBinding()] +Param ( +) + +if ($IsMacOS -or $IsLinux) { + # We only package up for insertions on Windows agents since they are where optprof can happen. + Write-Verbose "Skipping VSInsertion artifact since we're not on Windows." + return @{} +} + +$RepoRoot = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..") +$BuildConfiguration = $env:BUILDCONFIGURATION +if (!$BuildConfiguration) { + $BuildConfiguration = 'Debug' +} + +$PackagesRoot = "$RepoRoot/bin/Packages/$BuildConfiguration" +$NuGetPackages = "$PackagesRoot/NuGet" +$VsixPackages = "$PackagesRoot/Vsix" +$AzurePipelinesPath = "$RepoRoot/azure-pipelines" +if ($env:BUILD_ARTIFACTSTAGINGDIRECTORY) { + $InsertionOutputs = Join-Path $env:BUILD_ARTIFACTSTAGINGDIRECTORY 'InsertionOutputs' +} + +if (!(Test-Path $NuGetPackages) -and !(Test-Path $VsixPackages)) { + Write-Warning "Skipping because NuGet and VSIX packages haven't been built yet." + return @{} +} + +$result = @{ + "$AzurePipelinesPath" = (Get-ChildItem "$AzurePipelinesPath/vs-insertion-script.ps1"); + "$NuGetPackages" = (Get-ChildItem $NuGetPackages -Recurse); +} + +if (Test-Path $VsixPackages) { + $result["$PackagesRoot"] += Get-ChildItem $VsixPackages -Recurse +} + +if ($InsertionOutputs -and $env:PROFILINGINPUTSPROPSNAME) { + $InsertionOutputsProfilingInputs = Join-Path $InsertionOutputs $env:PROFILINGINPUTSPROPSNAME + if (Test-Path -LiteralPath $InsertionOutputsProfilingInputs) { + $result[$InsertionOutputs] = Get-ChildItem -LiteralPath $InsertionOutputsProfilingInputs # OptProf ProfilingInputs + } +} + +$result diff --git a/azure-pipelines/artifacts/Variables.ps1 b/tools/artifacts/Variables.ps1 similarity index 80% rename from azure-pipelines/artifacts/Variables.ps1 rename to tools/artifacts/Variables.ps1 index c6330cd38..c4d976650 100644 --- a/azure-pipelines/artifacts/Variables.ps1 +++ b/tools/artifacts/Variables.ps1 @@ -2,13 +2,13 @@ # It "snaps" the values of these variables where we can compute them during the build, # and otherwise captures the scripts to run later during an Azure Pipelines environment release. -$RepoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot (Join-Path .. ..))) -$ArtifactBasePath = Join-Path $RepoRoot (Join-Path obj _artifacts) +$RepoRoot = [System.IO.Path]::GetFullPath("$PSScriptRoot/../..") +$ArtifactBasePath = "$RepoRoot/obj/_artifacts" $VariablesArtifactPath = Join-Path $ArtifactBasePath variables if (-not (Test-Path $VariablesArtifactPath)) { New-Item -ItemType Directory -Path $VariablesArtifactPath | Out-Null } # Copy variables, either by value if the value is calculable now, or by script -Get-ChildItem -Path (Join-Path $PSScriptRoot (Join-Path .. variables)) |% { +Get-ChildItem "$PSScriptRoot/../variables" |% { $value = $null if (-not $_.BaseName.StartsWith('_')) { # Skip trying to interpret special scripts # First check the environment variables in case the variable was set in a queued build @@ -26,16 +26,16 @@ Get-ChildItem -Path (Join-Path $PSScriptRoot (Join-Path .. variables)) |% { if ($value) { # We got something, so wrap it with quotes so it's treated like a literal value. - $value = "'$value'" + $value = "'" + $value.Replace("'", "''") + "'" } } # If that didn't get us anything, just copy the script itself if (-not $value) { - $value = Get-Content -Path $_.FullName + $value = Get-Content -LiteralPath $_.FullName } - Set-Content -Path (Join-Path $VariablesArtifactPath $_.Name) -Value $value + Set-Content -LiteralPath "$VariablesArtifactPath/$($_.Name)" -Value $value } @{ diff --git a/azure-pipelines/artifacts/_all.ps1 b/tools/artifacts/_all.ps1 old mode 100755 new mode 100644 similarity index 86% rename from azure-pipelines/artifacts/_all.ps1 rename to tools/artifacts/_all.ps1 index afe42be30..9a22a1d08 --- a/azure-pipelines/artifacts/_all.ps1 +++ b/tools/artifacts/_all.ps1 @@ -12,9 +12,10 @@ Value = an array of paths (absolute or relative to the BaseDirectory) to files to include in the artifact. FileInfo objects are also allowed. .PARAMETER Force - Executes artifact scripts even if they have already been uploaded. + Executes artifact scripts even if they have already been staged. #> +[CmdletBinding(SupportsShouldProcess = $true)] param ( [string]$ArtifactNameSuffix, [switch]$Force @@ -28,15 +29,16 @@ Function EnsureTrailingSlash($path) { $path.Replace('\', [IO.Path]::DirectorySeparatorChar) } -Function Test-ArtifactUploaded($artifactName) { - $varName = "ARTIFACTUPLOADED_$($artifactName.ToUpper())" +Function Test-ArtifactStaged($artifactName) { + $varName = "ARTIFACTSTAGED_$($artifactName.ToUpper())" Test-Path "env:$varName" } Get-ChildItem "$PSScriptRoot\*.ps1" -Exclude "_*" -Recurse | % { $ArtifactName = $_.BaseName - if ($Force -or !(Test-ArtifactUploaded($ArtifactName + $ArtifactNameSuffix))) { + if ($Force -or !(Test-ArtifactStaged($ArtifactName + $ArtifactNameSuffix))) { $totalFileCount = 0 + Write-Verbose "Collecting file list for artifact $($_.BaseName)" $fileGroups = & $_ if ($fileGroups) { $fileGroups.GetEnumerator() | % { @@ -65,6 +67,6 @@ Get-ChildItem "$PSScriptRoot\*.ps1" -Exclude "_*" -Recurse | % { Write-Warning "No files found for the `"$ArtifactName`" artifact." } } else { - Write-Host "Skipping $ArtifactName because it has already been uploaded." -ForegroundColor DarkGray + Write-Host "Skipping $ArtifactName because it has already been staged." -ForegroundColor DarkGray } } diff --git a/tools/artifacts/_stage_all.ps1 b/tools/artifacts/_stage_all.ps1 new file mode 100644 index 000000000..bf961ce57 --- /dev/null +++ b/tools/artifacts/_stage_all.ps1 @@ -0,0 +1,72 @@ +<# +.SYNOPSIS + This script links all the artifacts described by _all.ps1 + into a staging directory, reading for uploading to a cloud build artifact store. + It returns a sequence of objects with Name and Path properties. +#> + +[CmdletBinding()] +param ( + [string]$ArtifactNameSuffix, + [switch]$AvoidSymbolicLinks +) + +$ArtifactStagingFolder = & "$PSScriptRoot/../Get-ArtifactsStagingDirectory.ps1" -CleanIfLocal + +function Create-SymbolicLink { + param ( + $Link, + $Target + ) + + if ($Link -eq $Target) { + return + } + + if (Test-Path $Link) { Remove-Item $Link } + $LinkContainer = Split-Path $Link -Parent + if (!(Test-Path $LinkContainer)) { mkdir $LinkContainer } + if ($IsMacOS -or $IsLinux) { + ln $Target $Link | Out-Null + } else { + cmd /c "mklink `"$Link`" `"$Target`"" | Out-Null + } + + if ($LASTEXITCODE -ne 0) { + # Windows requires admin privileges to create symbolic links + # unless Developer Mode has been enabled. + throw "Failed to create symbolic link at $Link that points to $Target" + } +} + +# Stage all artifacts +$Artifacts = & "$PSScriptRoot\_all.ps1" -ArtifactNameSuffix $ArtifactNameSuffix +$Artifacts |% { + $DestinationFolder = [System.IO.Path]::GetFullPath("$ArtifactStagingFolder/$($_.ArtifactName)$ArtifactNameSuffix/$($_.ContainerFolder)").TrimEnd('\') + $Name = "$(Split-Path $_.Source -Leaf)" + + #Write-Host "$($_.Source) -> $($_.ArtifactName)\$($_.ContainerFolder)" -ForegroundColor Yellow + + if (-not (Test-Path $DestinationFolder)) { New-Item -ItemType Directory -Path $DestinationFolder | Out-Null } + if (Test-Path -PathType Leaf $_.Source) { # skip folders + $TargetPath = Join-Path $DestinationFolder $Name + if ($AvoidSymbolicLinks) { + Copy-Item -LiteralPath $_.Source -Destination $TargetPath + } else { + Create-SymbolicLink -Link $TargetPath -Target $_.Source + } + } +} + +$ArtifactNames = $Artifacts |% { "$($_.ArtifactName)$ArtifactNameSuffix" } +$ArtifactNames += Get-ChildItem env:ARTIFACTSTAGED_* |% { + # Return from ALLCAPS to the actual capitalization used for the artifact. + $artifactNameAllCaps = "$($_.Name.Substring('ARTIFACTSTAGED_'.Length))" + (Get-ChildItem $ArtifactStagingFolder\$artifactNameAllCaps* -Filter $artifactNameAllCaps).Name +} +$ArtifactNames | Get-Unique |% { + $artifact = New-Object -TypeName PSObject + Add-Member -InputObject $artifact -MemberType NoteProperty -Name Name -Value $_ + Add-Member -InputObject $artifact -MemberType NoteProperty -Name Path -Value (Join-Path $ArtifactStagingFolder $_) + Write-Output $artifact +} diff --git a/tools/artifacts/build_logs.ps1 b/tools/artifacts/build_logs.ps1 new file mode 100644 index 000000000..f05358e03 --- /dev/null +++ b/tools/artifacts/build_logs.ps1 @@ -0,0 +1,7 @@ +$ArtifactStagingFolder = & "$PSScriptRoot/../Get-ArtifactsStagingDirectory.ps1" + +if (!(Test-Path $ArtifactStagingFolder/build_logs)) { return } + +@{ + "$ArtifactStagingFolder/build_logs" = (Get-ChildItem -Recurse "$ArtifactStagingFolder/build_logs") +} diff --git a/tools/artifacts/coverageResults.ps1 b/tools/artifacts/coverageResults.ps1 new file mode 100644 index 000000000..1aadbb747 --- /dev/null +++ b/tools/artifacts/coverageResults.ps1 @@ -0,0 +1,26 @@ +$RepoRoot = Resolve-Path "$PSScriptRoot\..\.." + +$coverageFilesUnderRoot = @(Get-ChildItem "$RepoRoot/*.cobertura.xml" -Recurse | Where-Object {$_.FullName -notlike "*/In/*" -and $_.FullName -notlike "*\In\*" }) + +# Under MTP, coverage files are written directly to the artifacts output directory, +# so we need to look there too. +$ArtifactStagingFolder = & "$PSScriptRoot/../Get-ArtifactsStagingDirectory.ps1" +$directTestLogs = Join-Path $ArtifactStagingFolder test_logs +$coverageFilesUnderArtifacts = if (Test-Path $directTestLogs) { @(Get-ChildItem "$directTestLogs/*.cobertura.xml" -Recurse) } else { @() } + +# Prepare code coverage reports for merging on another machine +Write-Host "Substituting $repoRoot with `"{reporoot}`"" +@($coverageFilesUnderRoot + $coverageFilesUnderArtifacts) |? { $_ }|% { + $content = Get-Content -LiteralPath $_ |% { $_ -Replace [regex]::Escape($repoRoot), "{reporoot}" } + Set-Content -LiteralPath $_ -Value $content -Encoding UTF8 +} + +if (!((Test-Path $RepoRoot\bin) -and (Test-Path $RepoRoot\obj))) { return } + +@{ + $directTestLogs = $coverageFilesUnderArtifacts; + $RepoRoot = ( + $coverageFilesUnderRoot + + (Get-ChildItem "$RepoRoot\obj\*.cs" -Recurse) + ); +} diff --git a/azure-pipelines/artifacts/deployables.ps1 b/tools/artifacts/deployables.ps1 similarity index 95% rename from azure-pipelines/artifacts/deployables.ps1 rename to tools/artifacts/deployables.ps1 index 6d8330def..ee9f3ec8b 100644 --- a/azure-pipelines/artifacts/deployables.ps1 +++ b/tools/artifacts/deployables.ps1 @@ -17,7 +17,7 @@ if (Test-Path $SosThreadingToolsRoot) { $ArchiveLayout = "$RepoRoot\obj\SosThreadingTools\ArchiveLayout" if (Test-Path $ArchiveLayout) { Remove-Item -Force $ArchiveLayout -Recurse } New-Item -Path $ArchiveLayout -ItemType Directory | Out-Null - Copy-Item -Force -Path "$SosThreadingToolsRoot" -Recurse -Exclude "DllExport.dll","*.xml" -Destination $ArchiveLayout + Copy-Item -Force -Path "$SosThreadingToolsRoot" -Recurse -Exclude "*.xml" -Destination $ArchiveLayout Rename-Item -Path $ArchiveLayout\net472 $ArchiveLayout\SosThreadingTools Get-ChildItem -Path $ArchiveLayout\symstore -Recurse | Remove-Item Compress-Archive -Force -Path $ArchiveLayout\SosThreadingTools -DestinationPath $ArchivePath diff --git a/azure-pipelines/artifacts/projectAssetsJson.ps1 b/tools/artifacts/projectAssetsJson.ps1 similarity index 100% rename from azure-pipelines/artifacts/projectAssetsJson.ps1 rename to tools/artifacts/projectAssetsJson.ps1 diff --git a/tools/artifacts/symbols.ps1 b/tools/artifacts/symbols.ps1 new file mode 100644 index 000000000..91f83f0d4 --- /dev/null +++ b/tools/artifacts/symbols.ps1 @@ -0,0 +1,10 @@ +$BinPath = [System.IO.Path]::GetFullPath("$PSScriptRoot/../../bin") +$ExternalPath = [System.IO.Path]::GetFullPath("$PSScriptRoot/../../obj/SymbolsPackages") +if (!(Test-Path $BinPath)) { return } +$symbolfiles = & "$PSScriptRoot/../Get-SymbolFiles.ps1" -Path $BinPath | Get-Unique +$ExternalFiles = & "$PSScriptRoot/../Get-ExternalSymbolFiles.ps1" + +@{ + "$BinPath" = $SymbolFiles; + "$ExternalPath" = $ExternalFiles; +} diff --git a/tools/artifacts/testResults.ps1 b/tools/artifacts/testResults.ps1 new file mode 100644 index 000000000..1817ca09f --- /dev/null +++ b/tools/artifacts/testResults.ps1 @@ -0,0 +1,24 @@ +[CmdletBinding()] +Param( +) + +$result = @{} + +$RepoRoot = Resolve-Path "$PSScriptRoot\..\.." +$testRoot = Join-Path $RepoRoot test +$legacyTestResults = Join-Path $testRoot TestResults +if (Test-Path $legacyTestResults) { + $result[$testRoot] = Get-ChildItem $legacyTestResults -Recurse -Directory | + Get-ChildItem -Recurse -File | + Where-Object { $_.Extension -ne '.dmp' -or $_.FullName -match '[/\\]In[/\\]' } +} + +$artifactStaging = & "$PSScriptRoot/../Get-ArtifactsStagingDirectory.ps1" +$testlogsPath = Join-Path $artifactStaging "test_logs" +if (Test-Path $testlogsPath) { + # Hang and crash dumps are copied into the TRX attachment directory. + $result[$testlogsPath] = Get-ChildItem $testlogsPath -Recurse | + Where-Object { $_.Extension -ne '.dmp' -or $_.FullName -match '[/\\]In[/\\]' } +} + +$result diff --git a/azure-pipelines/artifacts/symbols.ps1 b/tools/artifacts/test_symbols.ps1 similarity index 72% rename from azure-pipelines/artifacts/symbols.ps1 rename to tools/artifacts/test_symbols.ps1 index 8704571ec..ce2b6481c 100644 --- a/azure-pipelines/artifacts/symbols.ps1 +++ b/tools/artifacts/test_symbols.ps1 @@ -1,5 +1,6 @@ $BinPath = [System.IO.Path]::GetFullPath("$PSScriptRoot/../../bin") -$symbolfiles = & "$PSScriptRoot/../Get-SymbolFiles.ps1" -Path $BinPath | Get-Unique +if (!(Test-Path $BinPath)) { return } +$symbolfiles = & "$PSScriptRoot/../Get-SymbolFiles.ps1" -Path $BinPath -Tests | Get-Unique @{ "$BinPath" = $SymbolFiles; diff --git a/tools/dirs.proj b/tools/dirs.proj new file mode 100644 index 000000000..8f1d6787a --- /dev/null +++ b/tools/dirs.proj @@ -0,0 +1,6 @@ + + + + + + diff --git a/tools/dotnet-install.ps1 b/tools/dotnet-install.ps1 new file mode 100644 index 000000000..e62264b16 --- /dev/null +++ b/tools/dotnet-install.ps1 @@ -0,0 +1,1356 @@ +# +# Copyright (c) .NET Foundation and contributors. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. +# + +<# +.SYNOPSIS + Installs dotnet cli +.DESCRIPTION + Installs dotnet cli. If dotnet installation already exists in the given directory + it will update it only if the requested version differs from the one already installed. + + Note that the intended use of this script is for Continuous Integration (CI) scenarios, where: + - The SDK needs to be installed without user interaction and without admin rights. + - The SDK installation doesn't need to persist across multiple CI runs. + To set up a development environment or to run apps, use installers rather than this script. Visit https://dotnet.microsoft.com/download to get the installer. + +.PARAMETER Channel + Default: LTS + Download from the Channel specified. Possible values: + - STS - the most recent Standard Term Support release + - LTS - the most recent Long Term Support release + - 2-part version in a format A.B - represents a specific release + examples: 2.0, 1.0 + - 3-part version in a format A.B.Cxx - represents a specific SDK release + examples: 5.0.1xx, 5.0.2xx + Supported since 5.0 release + Warning: Value "Current" is deprecated for the Channel parameter. Use "STS" instead. + Note: The version parameter overrides the channel parameter when any version other than 'latest' is used. +.PARAMETER Quality + Download the latest build of specified quality in the channel. The possible values are: daily, preview, GA. + Works only in combination with channel. Not applicable for STS and LTS channels and will be ignored if those channels are used. + For SDK use channel in A.B.Cxx format: using quality together with channel in A.B format is not supported. + Supported since 5.0 release. + Note: The version parameter overrides the channel parameter when any version other than 'latest' is used, and therefore overrides the quality. +.PARAMETER Version + Default: latest + Represents a build version on specific channel. Possible values: + - latest - the latest build on specific channel + - 3-part version in a format A.B.C - represents specific version of build + examples: 2.0.0-preview2-006120, 1.1.0 +.PARAMETER Internal + Download internal builds. Requires providing credentials via -FeedCredential parameter. +.PARAMETER FeedCredential + Token to access Azure feed. Used as a query string to append to the Azure feed. + This parameter typically is not specified. +.PARAMETER InstallDir + Default: %LocalAppData%\Microsoft\dotnet + Path to where to install dotnet. Note that binaries will be placed directly in a given directory. +.PARAMETER Architecture + Default: - this value represents currently running OS architecture + Architecture of dotnet binaries to be installed. + Possible values are: , amd64, x64, x86, arm64, arm +.PARAMETER SharedRuntime + This parameter is obsolete and may be removed in a future version of this script. + The recommended alternative is '-Runtime dotnet'. + Installs just the shared runtime bits, not the entire SDK. +.PARAMETER Runtime + Installs just a shared runtime, not the entire SDK. + Possible values: + - dotnet - the Microsoft.NETCore.App shared runtime + - aspnetcore - the Microsoft.AspNetCore.App shared runtime + - windowsdesktop - the Microsoft.WindowsDesktop.App shared runtime +.PARAMETER DryRun + If set it will not perform installation but instead display what command line to use to consistently install + currently requested version of dotnet cli. In example if you specify version 'latest' it will display a link + with specific version so that this command can be used deterministically in a build script. + It also displays binaries location if you prefer to install or download it yourself. +.PARAMETER NoPath + By default this script will set environment variable PATH for the current process to the binaries folder inside installation folder. + If set it will display binaries location but not set any environment variable. +.PARAMETER Verbose + Displays diagnostics information. +.PARAMETER AzureFeed + Default: https://builds.dotnet.microsoft.com/dotnet + For internal use only. + Allows using a different storage to download SDK archives from. +.PARAMETER UncachedFeed + For internal use only. + Allows using a different storage to download SDK archives from. +.PARAMETER ProxyAddress + If set, the installer will use the proxy when making web requests +.PARAMETER ProxyUseDefaultCredentials + Default: false + Use default credentials, when using proxy address. +.PARAMETER ProxyBypassList + If set with ProxyAddress, will provide the list of comma separated urls that will bypass the proxy +.PARAMETER SkipNonVersionedFiles + Default: false + Skips installing non-versioned files if they already exist, such as dotnet.exe. +.PARAMETER JSonFile + Determines the SDK version from a user specified global.json file + Note: global.json must have a value for 'SDK:Version' +.PARAMETER DownloadTimeout + Determines timeout duration in seconds for downloading of the SDK file + Default: 1200 seconds (20 minutes) +.PARAMETER KeepZip + If set, downloaded file is kept +.PARAMETER ZipPath + Use that path to store installer, generated by default +.EXAMPLE + dotnet-install.ps1 -Version 7.0.401 + Installs the .NET SDK version 7.0.401 +.EXAMPLE + dotnet-install.ps1 -Channel 8.0 -Quality GA + Installs the latest GA (general availability) version of the .NET 8.0 SDK +#> +[cmdletbinding()] +param( + [string]$Channel = "LTS", + [string]$Quality, + [string]$Version = "Latest", + [switch]$Internal, + [string]$JSonFile, + [Alias('i')][string]$InstallDir = "", + [string]$Architecture = "", + [string]$Runtime, + [Obsolete("This parameter may be removed in a future version of this script. The recommended alternative is '-Runtime dotnet'.")] + [switch]$SharedRuntime, + [switch]$DryRun, + [switch]$NoPath, + [string]$AzureFeed, + [string]$UncachedFeed, + [string]$FeedCredential, + [string]$ProxyAddress, + [switch]$ProxyUseDefaultCredentials, + [string[]]$ProxyBypassList = @(), + [switch]$SkipNonVersionedFiles, + [int]$DownloadTimeout = 1200, + [switch]$KeepZip, + [string]$ZipPath = [System.IO.Path]::combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName()), + [switch]$Help +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" + +function Say($str) { + try { + Write-Host "dotnet-install: $str" + } + catch { + # Some platforms cannot utilize Write-Host (Azure Functions, for instance). Fall back to Write-Output + Write-Output "dotnet-install: $str" + } +} + +function Say-Warning($str) { + try { + Write-Warning "dotnet-install: $str" + } + catch { + # Some platforms cannot utilize Write-Warning (Azure Functions, for instance). Fall back to Write-Output + Write-Output "dotnet-install: Warning: $str" + } +} + +# Writes a line with error style settings. +# Use this function to show a human-readable comment along with an exception. +function Say-Error($str) { + try { + # Write-Error is quite verbose for the purpose of the function, let's write one line with error style settings. + $Host.UI.WriteErrorLine("dotnet-install: $str") + } + catch { + Write-Output "dotnet-install: Error: $str" + } +} + +function Say-Verbose($str) { + try { + Write-Verbose "dotnet-install: $str" + } + catch { + # Some platforms cannot utilize Write-Verbose (Azure Functions, for instance). Fall back to Write-Output + Write-Output "dotnet-install: $str" + } +} + +function Measure-Action($name, $block) { + $time = Measure-Command $block + $totalSeconds = $time.TotalSeconds + Say-Verbose "Action '$name' took $totalSeconds seconds" +} + +function Get-Remote-File-Size($zipUri) { + try { + $response = Invoke-WebRequest -Uri $zipUri -Method Head + $fileSize = $response.Headers["Content-Length"] + if ((![string]::IsNullOrEmpty($fileSize))) { + Say "Remote file $zipUri size is $fileSize bytes." + + return $fileSize + } + } + catch { + Say-Verbose "Content-Length header was not extracted for $zipUri." + } + + return $null +} + +function Say-Invocation($Invocation) { + $command = $Invocation.MyCommand; + $args = (($Invocation.BoundParameters.Keys | foreach { "-$_ `"$($Invocation.BoundParameters[$_])`"" }) -join " ") + Say-Verbose "$command $args" +} + +function Invoke-With-Retry([ScriptBlock]$ScriptBlock, [System.Threading.CancellationToken]$cancellationToken = [System.Threading.CancellationToken]::None, [int]$MaxAttempts = 3, [int]$SecondsBetweenAttempts = 1) { + $Attempts = 0 + $local:startTime = $(get-date) + + while ($true) { + try { + return & $ScriptBlock + } + catch { + $Attempts++ + if (($Attempts -lt $MaxAttempts) -and -not $cancellationToken.IsCancellationRequested) { + Start-Sleep $SecondsBetweenAttempts + } + else { + $local:elapsedTime = $(get-date) - $local:startTime + if (($local:elapsedTime.TotalSeconds - $DownloadTimeout) -gt 0 -and -not $cancellationToken.IsCancellationRequested) { + throw New-Object System.TimeoutException("Failed to reach the server: connection timeout: default timeout is $DownloadTimeout second(s)"); + } + throw; + } + } + } +} + +function Get-Machine-Architecture() { + Say-Invocation $MyInvocation + + # On PS x86, PROCESSOR_ARCHITECTURE reports x86 even on x64 systems. + # To get the correct architecture, we need to use PROCESSOR_ARCHITEW6432. + # PS x64 doesn't define this, so we fall back to PROCESSOR_ARCHITECTURE. + # Possible values: amd64, x64, x86, arm64, arm + if ( $ENV:PROCESSOR_ARCHITEW6432 -ne $null ) { + return $ENV:PROCESSOR_ARCHITEW6432 + } + + try { + if ( ((Get-CimInstance -ClassName CIM_OperatingSystem).OSArchitecture) -like "ARM*") { + if ( [Environment]::Is64BitOperatingSystem ) { + return "arm64" + } + return "arm" + } + } + catch { + # Machine doesn't support Get-CimInstance + } + + return $ENV:PROCESSOR_ARCHITECTURE +} + +function Get-CLIArchitecture-From-Architecture([string]$Architecture) { + Say-Invocation $MyInvocation + + if ($Architecture -eq "") { + $Architecture = Get-Machine-Architecture + } + + switch ($Architecture.ToLowerInvariant()) { + { ($_ -eq "amd64") -or ($_ -eq "x64") } { return "x64" } + { $_ -eq "x86" } { return "x86" } + { $_ -eq "arm" } { return "arm" } + { $_ -eq "arm64" } { return "arm64" } + default { throw "Architecture '$Architecture' not supported. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues" } + } +} + +function ValidateFeedCredential([string] $FeedCredential) { + if ($Internal -and [string]::IsNullOrWhitespace($FeedCredential)) { + $message = "Provide credentials via -FeedCredential parameter." + if ($DryRun) { + Say-Warning "$message" + } + else { + throw "$message" + } + } + + #FeedCredential should start with "?", for it to be added to the end of the link. + #adding "?" at the beginning of the FeedCredential if needed. + if ((![string]::IsNullOrWhitespace($FeedCredential)) -and ($FeedCredential[0] -ne '?')) { + $FeedCredential = "?" + $FeedCredential + } + + return $FeedCredential +} +function Get-NormalizedQuality([string]$Quality) { + Say-Invocation $MyInvocation + + if ([string]::IsNullOrEmpty($Quality)) { + return "" + } + + switch ($Quality) { + { @("daily", "preview") -contains $_ } { return $Quality.ToLowerInvariant() } + #ga quality is available without specifying quality, so normalizing it to empty + { $_ -eq "ga" } { return "" } + default { throw "'$Quality' is not a supported value for -Quality option. Supported values are: daily, preview, ga. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues." } + } +} + +function Get-NormalizedChannel([string]$Channel) { + Say-Invocation $MyInvocation + + if ([string]::IsNullOrEmpty($Channel)) { + return "" + } + + if ($Channel.Contains("Current")) { + Say-Warning 'Value "Current" is deprecated for -Channel option. Use "STS" instead.' + } + + if ($Channel.StartsWith('release/')) { + Say-Warning 'Using branch name with -Channel option is no longer supported with newer releases. Use -Quality option with a channel in X.Y format instead, such as "-Channel 5.0 -Quality Daily."' + } + + switch ($Channel) { + { $_ -eq "lts" } { return "LTS" } + { $_ -eq "sts" } { return "STS" } + { $_ -eq "current" } { return "STS" } + default { return $Channel.ToLowerInvariant() } + } +} + +function Get-NormalizedProduct([string]$Runtime) { + Say-Invocation $MyInvocation + + switch ($Runtime) { + { $_ -eq "dotnet" } { return "dotnet-runtime" } + { $_ -eq "aspnetcore" } { return "aspnetcore-runtime" } + { $_ -eq "windowsdesktop" } { return "windowsdesktop-runtime" } + { [string]::IsNullOrEmpty($_) } { return "dotnet-sdk" } + default { throw "'$Runtime' is not a supported value for -Runtime option, supported values are: dotnet, aspnetcore, windowsdesktop. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues." } + } +} + + +# The version text returned from the feeds is a 1-line or 2-line string: +# For the SDK and the dotnet runtime (2 lines): +# Line 1: # commit_hash +# Line 2: # 4-part version +# For the aspnetcore runtime (1 line): +# Line 1: # 4-part version +function Get-Version-From-LatestVersion-File-Content([string]$VersionText) { + Say-Invocation $MyInvocation + + $Data = -split $VersionText + + $VersionInfo = @{ + CommitHash = $(if ($Data.Count -gt 1) { $Data[0] }) + Version = $Data[-1] # last line is always the version number. + } + return $VersionInfo +} + +function Load-Assembly([string] $Assembly) { + try { + Add-Type -Assembly $Assembly | Out-Null + } + catch { + # On Nano Server, Powershell Core Edition is used. Add-Type is unable to resolve base class assemblies because they are not GAC'd. + # Loading the base class assemblies is not unnecessary as the types will automatically get resolved. + } +} + +function GetHTTPResponse([Uri] $Uri, [bool]$HeaderOnly, [bool]$DisableRedirect, [bool]$DisableFeedCredential) { + $cts = New-Object System.Threading.CancellationTokenSource + + $downloadScript = { + + $HttpClient = $null + + try { + # HttpClient is used vs Invoke-WebRequest in order to support Nano Server which doesn't support the Invoke-WebRequest cmdlet. + Load-Assembly -Assembly System.Net.Http + + if (-not $ProxyAddress) { + try { + # Despite no proxy being explicitly specified, we may still be behind a default proxy + $DefaultProxy = [System.Net.WebRequest]::DefaultWebProxy; + if ($DefaultProxy -and (-not $DefaultProxy.IsBypassed($Uri))) { + if ($null -ne $DefaultProxy.GetProxy($Uri)) { + $ProxyAddress = $DefaultProxy.GetProxy($Uri).OriginalString + } + else { + $ProxyAddress = $null + } + $ProxyUseDefaultCredentials = $true + } + } + catch { + # Eat the exception and move forward as the above code is an attempt + # at resolving the DefaultProxy that may not have been a problem. + $ProxyAddress = $null + Say-Verbose("Exception ignored: $_.Exception.Message - moving forward...") + } + } + + $HttpClientHandler = New-Object System.Net.Http.HttpClientHandler + if ($ProxyAddress) { + $HttpClientHandler.Proxy = New-Object System.Net.WebProxy -Property @{ + Address = $ProxyAddress; + UseDefaultCredentials = $ProxyUseDefaultCredentials; + BypassList = $ProxyBypassList; + } + } + if ($DisableRedirect) { + $HttpClientHandler.AllowAutoRedirect = $false + } + $HttpClient = New-Object System.Net.Http.HttpClient -ArgumentList $HttpClientHandler + + # Default timeout for HttpClient is 100s. For a 50 MB download this assumes 500 KB/s average, any less will time out + # Defaulting to 20 minutes allows it to work over much slower connections. + $HttpClient.Timeout = New-TimeSpan -Seconds $DownloadTimeout + + if ($HeaderOnly) { + $completionOption = [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead + } + else { + $completionOption = [System.Net.Http.HttpCompletionOption]::ResponseContentRead + } + + if ($DisableFeedCredential) { + $UriWithCredential = $Uri + } + else { + $UriWithCredential = "${Uri}${FeedCredential}" + } + + $Task = $HttpClient.GetAsync("$UriWithCredential", $completionOption).ConfigureAwait("false"); + $Response = $Task.GetAwaiter().GetResult(); + + if (($null -eq $Response) -or ((-not $HeaderOnly) -and (-not ($Response.IsSuccessStatusCode)))) { + # The feed credential is potentially sensitive info. Do not log FeedCredential to console output. + $DownloadException = [System.Exception] "Unable to download $Uri." + + if ($null -ne $Response) { + $DownloadException.Data["StatusCode"] = [int] $Response.StatusCode + $DownloadException.Data["ErrorMessage"] = "Unable to download $Uri. Returned HTTP status code: " + $DownloadException.Data["StatusCode"] + + if (404 -eq [int] $Response.StatusCode) { + $cts.Cancel() + } + } + + throw $DownloadException + } + + return $Response + } + catch [System.Net.Http.HttpRequestException] { + $DownloadException = [System.Exception] "Unable to download $Uri." + + # Pick up the exception message and inner exceptions' messages if they exist + $CurrentException = $PSItem.Exception + $ErrorMsg = $CurrentException.Message + "`r`n" + while ($CurrentException.InnerException) { + $CurrentException = $CurrentException.InnerException + $ErrorMsg += $CurrentException.Message + "`r`n" + } + + # Check if there is an issue concerning TLS. + if ($ErrorMsg -like "*SSL/TLS*") { + $ErrorMsg += "Ensure that TLS 1.2 or higher is enabled to use this script.`r`n" + } + + $DownloadException.Data["ErrorMessage"] = $ErrorMsg + throw $DownloadException + } + finally { + if ($null -ne $HttpClient) { + $HttpClient.Dispose() + } + } + } + + try { + return Invoke-With-Retry $downloadScript $cts.Token + } + finally { + if ($null -ne $cts) { + $cts.Dispose() + } + } +} + +function Get-Version-From-LatestVersion-File([string]$AzureFeed, [string]$Channel) { + Say-Invocation $MyInvocation + + $VersionFileUrl = $null + if ($Runtime -eq "dotnet") { + $VersionFileUrl = "$AzureFeed/Runtime/$Channel/latest.version" + } + elseif ($Runtime -eq "aspnetcore") { + $VersionFileUrl = "$AzureFeed/aspnetcore/Runtime/$Channel/latest.version" + } + elseif ($Runtime -eq "windowsdesktop") { + $VersionFileUrl = "$AzureFeed/WindowsDesktop/$Channel/latest.version" + } + elseif (-not $Runtime) { + $VersionFileUrl = "$AzureFeed/Sdk/$Channel/latest.version" + } + else { + throw "Invalid value for `$Runtime" + } + + Say-Verbose "Constructed latest.version URL: $VersionFileUrl" + + try { + $Response = GetHTTPResponse -Uri $VersionFileUrl + } + catch { + Say-Verbose "Failed to download latest.version file." + throw + } + $StringContent = $Response.Content.ReadAsStringAsync().Result + + switch ($Response.Content.Headers.ContentType) { + { ($_ -eq "application/octet-stream") } { $VersionText = $StringContent } + { ($_ -eq "text/plain") } { $VersionText = $StringContent } + { ($_ -eq "text/plain; charset=UTF-8") } { $VersionText = $StringContent } + default { throw "``$Response.Content.Headers.ContentType`` is an unknown .version file content type." } + } + + $VersionInfo = Get-Version-From-LatestVersion-File-Content $VersionText + + return $VersionInfo +} + +function Parse-Jsonfile-For-Version([string]$JSonFile) { + Say-Invocation $MyInvocation + + If (-Not (Test-Path $JSonFile)) { + throw "Unable to find '$JSonFile'" + } + try { + $JSonContent = Get-Content($JSonFile) -Raw | ConvertFrom-Json | Select-Object -expand "sdk" -ErrorAction SilentlyContinue + } + catch { + Say-Error "Json file unreadable: '$JSonFile'" + throw + } + if ($JSonContent) { + try { + $JSonContent.PSObject.Properties | ForEach-Object { + $PropertyName = $_.Name + if ($PropertyName -eq "version") { + $Version = $_.Value + Say-Verbose "Version = $Version" + } + } + } + catch { + Say-Error "Unable to parse the SDK node in '$JSonFile'" + throw + } + } + else { + throw "Unable to find the SDK node in '$JSonFile'" + } + If ($Version -eq $null) { + throw "Unable to find the SDK:version node in '$JSonFile'" + } + return $Version +} + +function Get-Specific-Version-From-Version([string]$AzureFeed, [string]$Channel, [string]$Version, [string]$JSonFile) { + Say-Invocation $MyInvocation + + if (-not $JSonFile) { + if ($Version.ToLowerInvariant() -eq "latest") { + $LatestVersionInfo = Get-Version-From-LatestVersion-File -AzureFeed $AzureFeed -Channel $Channel + return $LatestVersionInfo.Version + } + else { + return $Version + } + } + else { + return Parse-Jsonfile-For-Version $JSonFile + } +} + +function Get-Download-Link([string]$AzureFeed, [string]$SpecificVersion, [string]$CLIArchitecture) { + Say-Invocation $MyInvocation + + # If anything fails in this lookup it will default to $SpecificVersion + $SpecificProductVersion = Get-Product-Version -AzureFeed $AzureFeed -SpecificVersion $SpecificVersion + + if ($Runtime -eq "dotnet") { + $PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/dotnet-runtime-$SpecificProductVersion-win-$CLIArchitecture.zip" + } + elseif ($Runtime -eq "aspnetcore") { + $PayloadURL = "$AzureFeed/aspnetcore/Runtime/$SpecificVersion/aspnetcore-runtime-$SpecificProductVersion-win-$CLIArchitecture.zip" + } + elseif ($Runtime -eq "windowsdesktop") { + # The windows desktop runtime is part of the core runtime layout prior to 5.0 + $PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/windowsdesktop-runtime-$SpecificProductVersion-win-$CLIArchitecture.zip" + if ($SpecificVersion -match '^(\d+)\.(.*)$') { + $majorVersion = [int]$Matches[1] + if ($majorVersion -ge 5) { + $PayloadURL = "$AzureFeed/WindowsDesktop/$SpecificVersion/windowsdesktop-runtime-$SpecificProductVersion-win-$CLIArchitecture.zip" + } + } + } + elseif (-not $Runtime) { + $PayloadURL = "$AzureFeed/Sdk/$SpecificVersion/dotnet-sdk-$SpecificProductVersion-win-$CLIArchitecture.zip" + } + else { + throw "Invalid value for `$Runtime" + } + + Say-Verbose "Constructed primary named payload URL: $PayloadURL" + + return $PayloadURL, $SpecificProductVersion +} + +function Get-LegacyDownload-Link([string]$AzureFeed, [string]$SpecificVersion, [string]$CLIArchitecture) { + Say-Invocation $MyInvocation + + if (-not $Runtime) { + $PayloadURL = "$AzureFeed/Sdk/$SpecificVersion/dotnet-dev-win-$CLIArchitecture.$SpecificVersion.zip" + } + elseif ($Runtime -eq "dotnet") { + $PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/dotnet-win-$CLIArchitecture.$SpecificVersion.zip" + } + else { + return $null + } + + Say-Verbose "Constructed legacy named payload URL: $PayloadURL" + + return $PayloadURL +} + +function Get-Product-Version([string]$AzureFeed, [string]$SpecificVersion, [string]$PackageDownloadLink) { + Say-Invocation $MyInvocation + + # Try to get the version number, using the productVersion.txt file located next to the installer file. + $ProductVersionTxtURLs = (Get-Product-Version-Url $AzureFeed $SpecificVersion $PackageDownloadLink -Flattened $true), + (Get-Product-Version-Url $AzureFeed $SpecificVersion $PackageDownloadLink -Flattened $false) + + Foreach ($ProductVersionTxtURL in $ProductVersionTxtURLs) { + Say-Verbose "Checking for the existence of $ProductVersionTxtURL" + + try { + $productVersionResponse = GetHTTPResponse($productVersionTxtUrl) + + if ($productVersionResponse.StatusCode -eq 200) { + $productVersion = $productVersionResponse.Content.ReadAsStringAsync().Result.Trim() + if ($productVersion -ne $SpecificVersion) { + Say "Using alternate version $productVersion found in $ProductVersionTxtURL" + } + return $productVersion + } + else { + Say-Verbose "Got StatusCode $($productVersionResponse.StatusCode) when trying to get productVersion.txt at $productVersionTxtUrl." + } + } + catch { + Say-Verbose "Could not read productVersion.txt at $productVersionTxtUrl (Exception: '$($_.Exception.Message)'. )" + } + } + + # Getting the version number with productVersion.txt has failed. Try parsing the download link for a version number. + if ([string]::IsNullOrEmpty($PackageDownloadLink)) { + Say-Verbose "Using the default value '$SpecificVersion' as the product version." + return $SpecificVersion + } + + $productVersion = Get-ProductVersionFromDownloadLink $PackageDownloadLink $SpecificVersion + return $productVersion +} + +function Get-Product-Version-Url([string]$AzureFeed, [string]$SpecificVersion, [string]$PackageDownloadLink, [bool]$Flattened) { + Say-Invocation $MyInvocation + + $majorVersion = $null + if ($SpecificVersion -match '^(\d+)\.(.*)') { + $majorVersion = $Matches[1] -as [int] + } + + $pvFileName = 'productVersion.txt' + if ($Flattened) { + if (-not $Runtime) { + $pvFileName = 'sdk-productVersion.txt' + } + elseif ($Runtime -eq "dotnet") { + $pvFileName = 'runtime-productVersion.txt' + } + else { + $pvFileName = "$Runtime-productVersion.txt" + } + } + + if ([string]::IsNullOrEmpty($PackageDownloadLink)) { + if ($Runtime -eq "dotnet") { + $ProductVersionTxtURL = "$AzureFeed/Runtime/$SpecificVersion/$pvFileName" + } + elseif ($Runtime -eq "aspnetcore") { + $ProductVersionTxtURL = "$AzureFeed/aspnetcore/Runtime/$SpecificVersion/$pvFileName" + } + elseif ($Runtime -eq "windowsdesktop") { + # The windows desktop runtime is part of the core runtime layout prior to 5.0 + $ProductVersionTxtURL = "$AzureFeed/Runtime/$SpecificVersion/$pvFileName" + if ($majorVersion -ne $null -and $majorVersion -ge 5) { + $ProductVersionTxtURL = "$AzureFeed/WindowsDesktop/$SpecificVersion/$pvFileName" + } + } + elseif (-not $Runtime) { + $ProductVersionTxtURL = "$AzureFeed/Sdk/$SpecificVersion/$pvFileName" + } + else { + throw "Invalid value '$Runtime' specified for `$Runtime" + } + } + else { + $ProductVersionTxtURL = $PackageDownloadLink.Substring(0, $PackageDownloadLink.LastIndexOf("/")) + "/$pvFileName" + } + + Say-Verbose "Constructed productVersion link: $ProductVersionTxtURL" + + return $ProductVersionTxtURL +} + +function Get-ProductVersionFromDownloadLink([string]$PackageDownloadLink, [string]$SpecificVersion) { + Say-Invocation $MyInvocation + + #product specific version follows the product name + #for filename 'dotnet-sdk-3.1.404-win-x64.zip': the product version is 3.1.400 + $filename = $PackageDownloadLink.Substring($PackageDownloadLink.LastIndexOf("/") + 1) + $filenameParts = $filename.Split('-') + if ($filenameParts.Length -gt 2) { + $productVersion = $filenameParts[2] + Say-Verbose "Extracted product version '$productVersion' from download link '$PackageDownloadLink'." + } + else { + Say-Verbose "Using the default value '$SpecificVersion' as the product version." + $productVersion = $SpecificVersion + } + return $productVersion +} + +function Get-User-Share-Path() { + Say-Invocation $MyInvocation + + $InstallRoot = $env:DOTNET_INSTALL_DIR + if (!$InstallRoot) { + $InstallRoot = "$env:LocalAppData\Microsoft\dotnet" + } + elseif ($InstallRoot -like "$env:ProgramFiles\dotnet\?*") { + Say-Warning "The install root specified by the environment variable DOTNET_INSTALL_DIR points to the sub folder of $env:ProgramFiles\dotnet which is the default dotnet install root using .NET SDK installer. It is better to keep aligned with .NET SDK installer." + } + return $InstallRoot +} + +function Resolve-Installation-Path([string]$InstallDir) { + Say-Invocation $MyInvocation + + if ($InstallDir -eq "") { + return Get-User-Share-Path + } + return $InstallDir +} + +function Test-User-Write-Access([string]$InstallDir) { + try { + $tempFileName = [guid]::NewGuid().ToString() + $tempFilePath = Join-Path -Path $InstallDir -ChildPath $tempFileName + New-Item -Path $tempFilePath -ItemType File -Force + Remove-Item $tempFilePath -Force + return $true + } + catch { + return $false + } +} + +function Is-Dotnet-Package-Installed([string]$InstallRoot, [string]$RelativePathToPackage, [string]$SpecificVersion) { + Say-Invocation $MyInvocation + + $DotnetPackagePath = Join-Path -Path $InstallRoot -ChildPath $RelativePathToPackage | Join-Path -ChildPath $SpecificVersion + Say-Verbose "Is-Dotnet-Package-Installed: DotnetPackagePath=$DotnetPackagePath" + return Test-Path $DotnetPackagePath -PathType Container +} + +function Get-Absolute-Path([string]$RelativeOrAbsolutePath) { + # Too much spam + # Say-Invocation $MyInvocation + + return $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($RelativeOrAbsolutePath) +} + +function Get-Path-Prefix-With-Version($path) { + # example path with regex: shared/1.0.0-beta-12345/somepath + $match = [regex]::match($path, "/\d+\.\d+[^/]+/") + if ($match.Success) { + return $entry.FullName.Substring(0, $match.Index + $match.Length) + } + + return $null +} + +function Get-List-Of-Directories-And-Versions-To-Unpack-From-Dotnet-Package([System.IO.Compression.ZipArchive]$Zip, [string]$OutPath) { + Say-Invocation $MyInvocation + + $ret = @() + foreach ($entry in $Zip.Entries) { + $dir = Get-Path-Prefix-With-Version $entry.FullName + if ($null -ne $dir) { + $path = Get-Absolute-Path $(Join-Path -Path $OutPath -ChildPath $dir) + if (-Not (Test-Path $path -PathType Container)) { + $ret += $dir + } + } + } + + $ret = $ret | Sort-Object | Get-Unique + + $values = ($ret | foreach { "$_" }) -join ";" + Say-Verbose "Directories to unpack: $values" + + return $ret +} + +# Example zip content and extraction algorithm: +# Rule: files if extracted are always being extracted to the same relative path locally +# .\ +# a.exe # file does not exist locally, extract +# b.dll # file exists locally, override only if $OverrideFiles set +# aaa\ # same rules as for files +# ... +# abc\1.0.0\ # directory contains version and exists locally +# ... # do not extract content under versioned part +# abc\asd\ # same rules as for files +# ... +# def\ghi\1.0.1\ # directory contains version and does not exist locally +# ... # extract content +function Extract-Dotnet-Package([string]$ZipPath, [string]$OutPath) { + Say-Invocation $MyInvocation + + Load-Assembly -Assembly System.IO.Compression.FileSystem + Set-Variable -Name Zip + try { + $Zip = [System.IO.Compression.ZipFile]::OpenRead($ZipPath) + + $DirectoriesToUnpack = Get-List-Of-Directories-And-Versions-To-Unpack-From-Dotnet-Package -Zip $Zip -OutPath $OutPath + + foreach ($entry in $Zip.Entries) { + $PathWithVersion = Get-Path-Prefix-With-Version $entry.FullName + if (($null -eq $PathWithVersion) -Or ($DirectoriesToUnpack -contains $PathWithVersion)) { + $DestinationPath = Get-Absolute-Path $(Join-Path -Path $OutPath -ChildPath $entry.FullName) + $DestinationDir = Split-Path -Parent $DestinationPath + $OverrideFiles = $OverrideNonVersionedFiles -Or (-Not (Test-Path $DestinationPath)) + if ((-Not $DestinationPath.EndsWith("\")) -And $OverrideFiles) { + New-Item -ItemType Directory -Force -Path $DestinationDir | Out-Null + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $DestinationPath, $OverrideNonVersionedFiles) + } + } + } + } + catch { + Say-Error "Failed to extract package. Exception: $_" + throw; + } + finally { + if ($null -ne $Zip) { + $Zip.Dispose() + } + } +} + +function DownloadFile($Source, [string]$OutPath) { + if ($Source -notlike "http*") { + # Using System.IO.Path.GetFullPath to get the current directory + # does not work in this context - $pwd gives the current directory + if (![System.IO.Path]::IsPathRooted($Source)) { + $Source = $(Join-Path -Path $pwd -ChildPath $Source) + } + $Source = Get-Absolute-Path $Source + Say "Copying file from $Source to $OutPath" + Copy-Item $Source $OutPath + return + } + + $Stream = $null + + try { + $Response = GetHTTPResponse -Uri $Source + $Stream = $Response.Content.ReadAsStreamAsync().Result + $File = [System.IO.File]::Create($OutPath) + $Stream.CopyTo($File) + $File.Close() + + ValidateRemoteLocalFileSizes -LocalFileOutPath $OutPath -SourceUri $Source + } + finally { + if ($null -ne $Stream) { + $Stream.Dispose() + } + } +} + +function ValidateRemoteLocalFileSizes([string]$LocalFileOutPath, $SourceUri) { + try { + $remoteFileSize = Get-Remote-File-Size -zipUri $SourceUri + $fileSize = [long](Get-Item $LocalFileOutPath).Length + Say "Downloaded file $SourceUri size is $fileSize bytes." + + if ((![string]::IsNullOrEmpty($remoteFileSize)) -and !([string]::IsNullOrEmpty($fileSize)) ) { + if ($remoteFileSize -ne $fileSize) { + Say "The remote and local file sizes are not equal. Remote file size is $remoteFileSize bytes and local size is $fileSize bytes. The local package may be corrupted." + } + else { + Say "The remote and local file sizes are equal." + } + } + else { + Say "Either downloaded or local package size can not be measured. One of them may be corrupted." + } + } + catch { + Say "Either downloaded or local package size can not be measured. One of them may be corrupted." + } +} + +function SafeRemoveFile($Path) { + try { + if (Test-Path $Path) { + Remove-Item $Path + Say-Verbose "The temporary file `"$Path`" was removed." + } + else { + Say-Verbose "The temporary file `"$Path`" does not exist, therefore is not removed." + } + } + catch { + Say-Warning "Failed to remove the temporary file: `"$Path`", remove it manually." + } +} + +function Prepend-Sdk-InstallRoot-To-Path([string]$InstallRoot) { + $BinPath = Get-Absolute-Path $(Join-Path -Path $InstallRoot -ChildPath "") + if (-Not $NoPath) { + $SuffixedBinPath = "$BinPath;" + if (-Not $env:path.Contains($SuffixedBinPath)) { + Say "Adding to current process PATH: `"$BinPath`". Note: This change will not be visible if PowerShell was run as a child process." + $env:path = $SuffixedBinPath + $env:path + } + else { + Say-Verbose "Current process PATH already contains `"$BinPath`"" + } + } + else { + Say "Binaries of dotnet can be found in $BinPath" + } +} + +function PrintDryRunOutput($Invocation, $DownloadLinks) { + Say "Payload URLs:" + + for ($linkIndex = 0; $linkIndex -lt $DownloadLinks.count; $linkIndex++) { + Say "URL #$linkIndex - $($DownloadLinks[$linkIndex].type): $($DownloadLinks[$linkIndex].downloadLink)" + } + $RepeatableCommand = ".\$ScriptName -Version `"$SpecificVersion`" -InstallDir `"$InstallRoot`" -Architecture `"$CLIArchitecture`"" + if ($Runtime -eq "dotnet") { + $RepeatableCommand += " -Runtime `"dotnet`"" + } + elseif ($Runtime -eq "aspnetcore") { + $RepeatableCommand += " -Runtime `"aspnetcore`"" + } + + foreach ($key in $Invocation.BoundParameters.Keys) { + if (-not (@("Architecture", "Channel", "DryRun", "InstallDir", "Runtime", "SharedRuntime", "Version", "Quality", "FeedCredential") -contains $key)) { + $RepeatableCommand += " -$key `"$($Invocation.BoundParameters[$key])`"" + } + } + if ($Invocation.BoundParameters.Keys -contains "FeedCredential") { + $RepeatableCommand += " -FeedCredential `"`"" + } + Say "Repeatable invocation: $RepeatableCommand" + if ($SpecificVersion -ne $EffectiveVersion) { + Say "NOTE: Due to finding a version manifest with this runtime, it would actually install with version '$EffectiveVersion'" + } +} + +function Get-AkaMSDownloadLink([string]$Channel, [string]$Quality, [bool]$Internal, [string]$Product, [string]$Architecture) { + Say-Invocation $MyInvocation + + #quality is not supported for LTS or STS channel + if (![string]::IsNullOrEmpty($Quality) -and (@("LTS", "STS") -contains $Channel)) { + $Quality = "" + Say-Warning "Specifying quality for STS or LTS channel is not supported, the quality will be ignored." + } + Say-Verbose "Retrieving primary payload URL from aka.ms link for channel: '$Channel', quality: '$Quality' product: '$Product', os: 'win', architecture: '$Architecture'." + + #construct aka.ms link + $akaMsLink = "https://aka.ms/dotnet" + if ($Internal) { + $akaMsLink += "/internal" + } + $akaMsLink += "/$Channel" + if (-not [string]::IsNullOrEmpty($Quality)) { + $akaMsLink += "/$Quality" + } + $akaMsLink += "/$Product-win-$Architecture.zip" + Say-Verbose "Constructed aka.ms link: '$akaMsLink'." + $akaMsDownloadLink = $null + + for ($maxRedirections = 9; $maxRedirections -ge 0; $maxRedirections--) { + #get HTTP response + #do not pass credentials as a part of the $akaMsLink and do not apply credentials in the GetHTTPResponse function + #otherwise the redirect link would have credentials as well + #it would result in applying credentials twice to the resulting link and thus breaking it, and in echoing credentials to the output as a part of redirect link + $Response = GetHTTPResponse -Uri $akaMsLink -HeaderOnly $true -DisableRedirect $true -DisableFeedCredential $true + Say-Verbose "Received response:`n$Response" + + if ([string]::IsNullOrEmpty($Response)) { + Say-Verbose "The link '$akaMsLink' is not valid: failed to get redirect location. The resource is not available." + return $null + } + + #if HTTP code is 301 (Moved Permanently), the redirect link exists + if ($Response.StatusCode -eq 301) { + try { + $akaMsDownloadLink = $Response.Headers.GetValues("Location")[0] + + if ([string]::IsNullOrEmpty($akaMsDownloadLink)) { + Say-Verbose "The link '$akaMsLink' is not valid: server returned 301 (Moved Permanently), but the headers do not contain the redirect location." + return $null + } + + Say-Verbose "The redirect location retrieved: '$akaMsDownloadLink'." + # This may yet be a link to another redirection. Attempt to retrieve the page again. + $akaMsLink = $akaMsDownloadLink + continue + } + catch { + Say-Verbose "The link '$akaMsLink' is not valid: failed to get redirect location." + return $null + } + } + elseif ((($Response.StatusCode -lt 300) -or ($Response.StatusCode -ge 400)) -and (-not [string]::IsNullOrEmpty($akaMsDownloadLink))) { + # Redirections have ended. + return $akaMsDownloadLink + } + + Say-Verbose "The link '$akaMsLink' is not valid: failed to retrieve the redirection location." + return $null + } + + Say-Verbose "Aka.ms links have redirected more than the maximum allowed redirections. This may be caused by a cyclic redirection of aka.ms links." + return $null + +} + +function Get-AkaMsLink-And-Version([string] $NormalizedChannel, [string] $NormalizedQuality, [bool] $Internal, [string] $ProductName, [string] $Architecture) { + $AkaMsDownloadLink = Get-AkaMSDownloadLink -Channel $NormalizedChannel -Quality $NormalizedQuality -Internal $Internal -Product $ProductName -Architecture $Architecture + + if ([string]::IsNullOrEmpty($AkaMsDownloadLink)) { + if (-not [string]::IsNullOrEmpty($NormalizedQuality)) { + # if quality is specified - exit with error - there is no fallback approach + Say-Error "Failed to locate the latest version in the channel '$NormalizedChannel' with '$NormalizedQuality' quality for '$ProductName', os: 'win', architecture: '$Architecture'." + Say-Error "Refer to: https://aka.ms/dotnet-os-lifecycle for information on .NET Core support." + throw "aka.ms link resolution failure" + } + Say-Verbose "Falling back to latest.version file approach." + return ($null, $null, $null) + } + else { + Say-Verbose "Retrieved primary named payload URL from aka.ms link: '$AkaMsDownloadLink'." + Say-Verbose "Downloading using legacy url will not be attempted." + + #get version from the path + $pathParts = $AkaMsDownloadLink.Split('/') + if ($pathParts.Length -ge 2) { + $SpecificVersion = $pathParts[$pathParts.Length - 2] + Say-Verbose "Version: '$SpecificVersion'." + } + else { + Say-Error "Failed to extract the version from download link '$AkaMsDownloadLink'." + return ($null, $null, $null) + } + + #retrieve effective (product) version + $EffectiveVersion = Get-Product-Version -SpecificVersion $SpecificVersion -PackageDownloadLink $AkaMsDownloadLink + Say-Verbose "Product version: '$EffectiveVersion'." + + return ($AkaMsDownloadLink, $SpecificVersion, $EffectiveVersion); + } +} + +function Get-Feeds-To-Use() { + $feeds = @( + "https://builds.dotnet.microsoft.com/dotnet" + "https://ci.dot.net/public" + ) + + if (-not [string]::IsNullOrEmpty($AzureFeed)) { + $feeds = @($AzureFeed) + } + + if (-not [string]::IsNullOrEmpty($UncachedFeed)) { + $feeds = @($UncachedFeed) + } + + Write-Verbose "Initialized feeds: $feeds" + + return $feeds +} + +function Resolve-AssetName-And-RelativePath([string] $Runtime) { + + if ($Runtime -eq "dotnet") { + $assetName = ".NET Core Runtime" + $dotnetPackageRelativePath = "shared\Microsoft.NETCore.App" + } + elseif ($Runtime -eq "aspnetcore") { + $assetName = "ASP.NET Core Runtime" + $dotnetPackageRelativePath = "shared\Microsoft.AspNetCore.App" + } + elseif ($Runtime -eq "windowsdesktop") { + $assetName = ".NET Core Windows Desktop Runtime" + $dotnetPackageRelativePath = "shared\Microsoft.WindowsDesktop.App" + } + elseif (-not $Runtime) { + $assetName = ".NET Core SDK" + $dotnetPackageRelativePath = "sdk" + } + else { + throw "Invalid value for `$Runtime" + } + + return ($assetName, $dotnetPackageRelativePath) +} + +function Prepare-Install-Directory { + $diskSpaceWarning = "Failed to check the disk space. Installation will continue, but it may fail if you do not have enough disk space."; + + if ($PSVersionTable.PSVersion.Major -lt 7) { + Say-Verbose $diskSpaceWarning + return + } + + New-Item -ItemType Directory -Force -Path $InstallRoot | Out-Null + + $installDrive = $((Get-Item $InstallRoot -Force).PSDrive.Name); + $diskInfo = $null + try { + $diskInfo = Get-PSDrive -Name $installDrive + } + catch { + Say-Warning $diskSpaceWarning + } + + # The check is relevant for PS version >= 7, the result can be irrelevant for older versions. See https://github.com/PowerShell/PowerShell/issues/12442. + if ( ($null -ne $diskInfo) -and ($diskInfo.Free / 1MB -le 100)) { + throw "There is not enough disk space on drive ${installDrive}:" + } +} + +if ($Help) { + Get-Help $PSCommandPath -Examples + exit +} + +Say-Verbose "Note that the intended use of this script is for Continuous Integration (CI) scenarios, where:" +Say-Verbose "- The SDK needs to be installed without user interaction and without admin rights." +Say-Verbose "- The SDK installation doesn't need to persist across multiple CI runs." +Say-Verbose "To set up a development environment or to run apps, use installers rather than this script. Visit https://dotnet.microsoft.com/download to get the installer.`r`n" + +if ($SharedRuntime -and (-not $Runtime)) { + $Runtime = "dotnet" +} + +$OverrideNonVersionedFiles = !$SkipNonVersionedFiles + +Measure-Action "Product discovery" { + $script:CLIArchitecture = Get-CLIArchitecture-From-Architecture $Architecture + $script:NormalizedQuality = Get-NormalizedQuality $Quality + Say-Verbose "Normalized quality: '$NormalizedQuality'" + $script:NormalizedChannel = Get-NormalizedChannel $Channel + Say-Verbose "Normalized channel: '$NormalizedChannel'" + $script:NormalizedProduct = Get-NormalizedProduct $Runtime + Say-Verbose "Normalized product: '$NormalizedProduct'" + $script:FeedCredential = ValidateFeedCredential $FeedCredential +} + +$InstallRoot = Resolve-Installation-Path $InstallDir +if (-not (Test-User-Write-Access $InstallRoot)) { + Say-Error "The current user doesn't have write access to the installation root '$InstallRoot' to install .NET. Please try specifying a different installation directory using the -InstallDir parameter, or ensure the selected directory has the appropriate permissions." + throw +} +Say-Verbose "InstallRoot: $InstallRoot" +$ScriptName = $MyInvocation.MyCommand.Name +($assetName, $dotnetPackageRelativePath) = Resolve-AssetName-And-RelativePath -Runtime $Runtime + +$feeds = Get-Feeds-To-Use +$DownloadLinks = @() + +if ($Version.ToLowerInvariant() -ne "latest" -and -not [string]::IsNullOrEmpty($Quality)) { + throw "Quality and Version options are not allowed to be specified simultaneously. See https:// learn.microsoft.com/dotnet/core/tools/dotnet-install-script#options for details." +} + +# aka.ms links can only be used if the user did not request a specific version via the command line or a global.json file. +if ([string]::IsNullOrEmpty($JSonFile) -and ($Version -eq "latest")) { + ($DownloadLink, $SpecificVersion, $EffectiveVersion) = Get-AkaMsLink-And-Version $NormalizedChannel $NormalizedQuality $Internal $NormalizedProduct $CLIArchitecture + + if ($null -ne $DownloadLink) { + $DownloadLinks += New-Object PSObject -Property @{downloadLink = "$DownloadLink"; specificVersion = "$SpecificVersion"; effectiveVersion = "$EffectiveVersion"; type = 'aka.ms' } + Say-Verbose "Generated aka.ms link $DownloadLink with version $EffectiveVersion" + + if (-Not $DryRun) { + Say-Verbose "Checking if the version $EffectiveVersion is already installed" + if (Is-Dotnet-Package-Installed -InstallRoot $InstallRoot -RelativePathToPackage $dotnetPackageRelativePath -SpecificVersion $EffectiveVersion) { + Say "$assetName with version '$EffectiveVersion' is already installed." + Prepend-Sdk-InstallRoot-To-Path -InstallRoot $InstallRoot + return + } + } + } +} + +# Primary and legacy links cannot be used if a quality was specified. +# If we already have an aka.ms link, no need to search the blob feeds. +if ([string]::IsNullOrEmpty($NormalizedQuality) -and 0 -eq $DownloadLinks.count) { + foreach ($feed in $feeds) { + try { + $SpecificVersion = Get-Specific-Version-From-Version -AzureFeed $feed -Channel $Channel -Version $Version -JSonFile $JSonFile + $DownloadLink, $EffectiveVersion = Get-Download-Link -AzureFeed $feed -SpecificVersion $SpecificVersion -CLIArchitecture $CLIArchitecture + $LegacyDownloadLink = Get-LegacyDownload-Link -AzureFeed $feed -SpecificVersion $SpecificVersion -CLIArchitecture $CLIArchitecture + + $DownloadLinks += New-Object PSObject -Property @{downloadLink = "$DownloadLink"; specificVersion = "$SpecificVersion"; effectiveVersion = "$EffectiveVersion"; type = 'primary' } + Say-Verbose "Generated primary link $DownloadLink with version $EffectiveVersion" + + if (-not [string]::IsNullOrEmpty($LegacyDownloadLink)) { + $DownloadLinks += New-Object PSObject -Property @{downloadLink = "$LegacyDownloadLink"; specificVersion = "$SpecificVersion"; effectiveVersion = "$EffectiveVersion"; type = 'legacy' } + Say-Verbose "Generated legacy link $LegacyDownloadLink with version $EffectiveVersion" + } + + if (-Not $DryRun) { + Say-Verbose "Checking if the version $EffectiveVersion is already installed" + if (Is-Dotnet-Package-Installed -InstallRoot $InstallRoot -RelativePathToPackage $dotnetPackageRelativePath -SpecificVersion $EffectiveVersion) { + Say "$assetName with version '$EffectiveVersion' is already installed." + Prepend-Sdk-InstallRoot-To-Path -InstallRoot $InstallRoot + return + } + } + } + catch { + Say-Verbose "Failed to acquire download links from feed $feed. Exception: $_" + } + } +} + +if ($DownloadLinks.count -eq 0) { + throw "Failed to resolve the exact version number." +} + +if ($DryRun) { + PrintDryRunOutput $MyInvocation $DownloadLinks + return +} + +Measure-Action "Installation directory preparation" { Prepare-Install-Directory } + +Say-Verbose "Zip path: $ZipPath" + +$DownloadSucceeded = $false +$DownloadedLink = $null +$ErrorMessages = @() + +foreach ($link in $DownloadLinks) { + Say-Verbose "Downloading `"$($link.type)`" link $($link.downloadLink)" + + try { + Measure-Action "Package download" { DownloadFile -Source $link.downloadLink -OutPath $ZipPath } + Say-Verbose "Download succeeded." + $DownloadSucceeded = $true + $DownloadedLink = $link + break + } + catch { + $StatusCode = $null + $ErrorMessage = $null + + if ($PSItem.Exception.Data.Contains("StatusCode")) { + $StatusCode = $PSItem.Exception.Data["StatusCode"] + } + + if ($PSItem.Exception.Data.Contains("ErrorMessage")) { + $ErrorMessage = $PSItem.Exception.Data["ErrorMessage"] + } + else { + $ErrorMessage = $PSItem.Exception.Message + } + + Say-Verbose "Download failed with status code $StatusCode. Error message: $ErrorMessage" + $ErrorMessages += "Downloading from `"$($link.type)`" link has failed with error:`nUri: $($link.downloadLink)`nStatusCode: $StatusCode`nError: $ErrorMessage" + } + + # This link failed. Clean up before trying the next one. + SafeRemoveFile -Path $ZipPath +} + +if (-not $DownloadSucceeded) { + foreach ($ErrorMessage in $ErrorMessages) { + Say-Error $ErrorMessages + } + + throw "Could not find `"$assetName`" with version = $($DownloadLinks[0].effectiveVersion)`nRefer to: https://aka.ms/dotnet-os-lifecycle for information on .NET support" +} + +Say "Extracting the archive." +Measure-Action "Package extraction" { Extract-Dotnet-Package -ZipPath $ZipPath -OutPath $InstallRoot } + +# Check if the SDK version is installed; if not, fail the installation. +$isAssetInstalled = $false + +# if the version contains "RTM" or "servicing"; check if a 'release-type' SDK version is installed. +if ($DownloadedLink.effectiveVersion -Match "rtm" -or $DownloadedLink.effectiveVersion -Match "servicing") { + $ReleaseVersion = $DownloadedLink.effectiveVersion.Split("-")[0] + Say-Verbose "Checking installation: version = $ReleaseVersion" + $isAssetInstalled = Is-Dotnet-Package-Installed -InstallRoot $InstallRoot -RelativePathToPackage $dotnetPackageRelativePath -SpecificVersion $ReleaseVersion +} + +# Check if the SDK version is installed. +if (!$isAssetInstalled) { + Say-Verbose "Checking installation: version = $($DownloadedLink.effectiveVersion)" + $isAssetInstalled = Is-Dotnet-Package-Installed -InstallRoot $InstallRoot -RelativePathToPackage $dotnetPackageRelativePath -SpecificVersion $DownloadedLink.effectiveVersion +} + +# Version verification failed. More likely something is wrong either with the downloaded content or with the verification algorithm. +if (!$isAssetInstalled) { + Say-Error "Failed to verify the version of installed `"$assetName`".`nInstallation source: $($DownloadedLink.downloadLink).`nInstallation location: $InstallRoot.`nReport the bug at https://github.com/dotnet/install-scripts/issues." + throw "`"$assetName`" with version = $($DownloadedLink.effectiveVersion) failed to install with an unknown error." +} + +if (-not $KeepZip) { + SafeRemoveFile -Path $ZipPath +} + +Measure-Action "Setting up shell environment" { Prepend-Sdk-InstallRoot-To-Path -InstallRoot $InstallRoot } + +Say "Note that the script does not ensure your Windows version is supported during the installation." +Say "To check the list of supported versions, go to https://learn.microsoft.com/dotnet/core/install/windows#supported-versions" +Say "Installed version is $($DownloadedLink.effectiveVersion)" +Say "Installation finished" diff --git a/tools/dotnet-install.sh b/tools/dotnet-install.sh new file mode 100755 index 000000000..6180745ea --- /dev/null +++ b/tools/dotnet-install.sh @@ -0,0 +1,1888 @@ +#!/usr/bin/env bash +# Copyright (c) .NET Foundation and contributors. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. +# + +# Stop script on NZEC +set -e +# Stop script if unbound variable found (use ${var:-} if intentional) +set -u +# By default cmd1 | cmd2 returns exit code of cmd2 regardless of cmd1 success +# This is causing it to fail +set -o pipefail + +# Use in the the functions: eval $invocation +invocation='say_verbose "Calling: ${yellow:-}${FUNCNAME[0]} ${green:-}$*${normal:-}"' + +# standard output may be used as a return value in the functions +# we need a way to write text on the screen in the functions so that +# it won't interfere with the return value. +# Exposing stream 3 as a pipe to standard output of the script itself +exec 3>&1 + +# Setup some colors to use. These need to work in fairly limited shells, like the Ubuntu Docker container where there are only 8 colors. +# See if stdout is a terminal +if [ -t 1 ] && command -v tput > /dev/null; then + # see if it supports colors + ncolors=$(tput colors || echo 0) + if [ -n "$ncolors" ] && [ $ncolors -ge 8 ]; then + bold="$(tput bold || echo)" + normal="$(tput sgr0 || echo)" + black="$(tput setaf 0 || echo)" + red="$(tput setaf 1 || echo)" + green="$(tput setaf 2 || echo)" + yellow="$(tput setaf 3 || echo)" + blue="$(tput setaf 4 || echo)" + magenta="$(tput setaf 5 || echo)" + cyan="$(tput setaf 6 || echo)" + white="$(tput setaf 7 || echo)" + fi +fi + +say_warning() { + printf "%b\n" "${yellow:-}dotnet_install: Warning: $1${normal:-}" >&3 +} + +say_err() { + printf "%b\n" "${red:-}dotnet_install: Error: $1${normal:-}" >&2 +} + +say() { + # using stream 3 (defined in the beginning) to not interfere with stdout of functions + # which may be used as return value + printf "%b\n" "${cyan:-}dotnet-install:${normal:-} $1" >&3 +} + +say_verbose() { + if [ "$verbose" = true ]; then + say "$1" + fi +} + +# This platform list is finite - if the SDK/Runtime has supported Linux distribution-specific assets, +# then and only then should the Linux distribution appear in this list. +# Adding a Linux distribution to this list does not imply distribution-specific support. +get_legacy_os_name_from_platform() { + eval $invocation + + platform="$1" + case "$platform" in + "centos.7") + echo "centos" + return 0 + ;; + "debian.8") + echo "debian" + return 0 + ;; + "debian.9") + echo "debian.9" + return 0 + ;; + "fedora.23") + echo "fedora.23" + return 0 + ;; + "fedora.24") + echo "fedora.24" + return 0 + ;; + "fedora.27") + echo "fedora.27" + return 0 + ;; + "fedora.28") + echo "fedora.28" + return 0 + ;; + "opensuse.13.2") + echo "opensuse.13.2" + return 0 + ;; + "opensuse.42.1") + echo "opensuse.42.1" + return 0 + ;; + "opensuse.42.3") + echo "opensuse.42.3" + return 0 + ;; + "rhel.7"*) + echo "rhel" + return 0 + ;; + "ubuntu.14.04") + echo "ubuntu" + return 0 + ;; + "ubuntu.16.04") + echo "ubuntu.16.04" + return 0 + ;; + "ubuntu.16.10") + echo "ubuntu.16.10" + return 0 + ;; + "ubuntu.18.04") + echo "ubuntu.18.04" + return 0 + ;; + "alpine.3.4.3") + echo "alpine" + return 0 + ;; + esac + return 1 +} + +get_legacy_os_name() { + eval $invocation + + local uname=$(uname) + if [ "$uname" = "Darwin" ]; then + echo "osx" + return 0 + elif [ -n "$runtime_id" ]; then + echo $(get_legacy_os_name_from_platform "${runtime_id%-*}" || echo "${runtime_id%-*}") + return 0 + else + if [ -e /etc/os-release ]; then + . /etc/os-release + os=$(get_legacy_os_name_from_platform "$ID${VERSION_ID:+.${VERSION_ID}}" || echo "") + if [ -n "$os" ]; then + echo "$os" + return 0 + fi + fi + fi + + say_verbose "Distribution specific OS name and version could not be detected: UName = $uname" + return 1 +} + +get_linux_platform_name() { + eval $invocation + + if [ -n "$runtime_id" ]; then + echo "${runtime_id%-*}" + return 0 + else + if [ -e /etc/os-release ]; then + . /etc/os-release + echo "$ID${VERSION_ID:+.${VERSION_ID}}" + return 0 + elif [ -e /etc/redhat-release ]; then + local redhatRelease=$(&1 || true) | grep -q musl +} + +get_current_os_name() { + eval $invocation + + local uname=$(uname) + if [ "$uname" = "Darwin" ]; then + echo "osx" + return 0 + elif [ "$uname" = "FreeBSD" ]; then + echo "freebsd" + return 0 + elif [ "$uname" = "Linux" ]; then + local linux_platform_name="" + linux_platform_name="$(get_linux_platform_name)" || true + + if [ "$linux_platform_name" = "rhel.6" ]; then + echo $linux_platform_name + return 0 + elif is_musl_based_distro; then + echo "linux-musl" + return 0 + elif [ "$linux_platform_name" = "linux-musl" ]; then + echo "linux-musl" + return 0 + else + echo "linux" + return 0 + fi + fi + + say_err "OS name could not be detected: UName = $uname" + return 1 +} + +machine_has() { + eval $invocation + + command -v "$1" > /dev/null 2>&1 + return $? +} + +check_min_reqs() { + local hasMinimum=false + if machine_has "curl"; then + hasMinimum=true + elif machine_has "wget"; then + hasMinimum=true + fi + + if [ "$hasMinimum" = "false" ]; then + say_err "curl (recommended) or wget are required to download dotnet. Install missing prerequisite to proceed." + return 1 + fi + return 0 +} + +# args: +# input - $1 +to_lowercase() { + #eval $invocation + + echo "$1" | tr '[:upper:]' '[:lower:]' + return 0 +} + +# args: +# input - $1 +remove_trailing_slash() { + #eval $invocation + + local input="${1:-}" + echo "${input%/}" + return 0 +} + +# args: +# input - $1 +remove_beginning_slash() { + #eval $invocation + + local input="${1:-}" + echo "${input#/}" + return 0 +} + +# args: +# root_path - $1 +# child_path - $2 - this parameter can be empty +combine_paths() { + eval $invocation + + # TODO: Consider making it work with any number of paths. For now: + if [ ! -z "${3:-}" ]; then + say_err "combine_paths: Function takes two parameters." + return 1 + fi + + local root_path="$(remove_trailing_slash "$1")" + local child_path="$(remove_beginning_slash "${2:-}")" + say_verbose "combine_paths: root_path=$root_path" + say_verbose "combine_paths: child_path=$child_path" + echo "$root_path/$child_path" + return 0 +} + +get_machine_architecture() { + eval $invocation + + if command -v uname > /dev/null; then + CPUName=$(uname -m) + case $CPUName in + armv1*|armv2*|armv3*|armv4*|armv5*|armv6*) + echo "armv6-or-below" + return 0 + ;; + armv*l) + echo "arm" + return 0 + ;; + aarch64|arm64) + if [ "$(getconf LONG_BIT)" -lt 64 ]; then + # This is 32-bit OS running on 64-bit CPU (for example Raspberry Pi OS) + echo "arm" + return 0 + fi + echo "arm64" + return 0 + ;; + s390x) + echo "s390x" + return 0 + ;; + ppc64le) + echo "ppc64le" + return 0 + ;; + loongarch64) + echo "loongarch64" + return 0 + ;; + riscv64) + echo "riscv64" + return 0 + ;; + powerpc|ppc) + echo "ppc" + return 0 + ;; + esac + fi + + # Always default to 'x64' + echo "x64" + return 0 +} + +# args: +# architecture - $1 +get_normalized_architecture_from_architecture() { + eval $invocation + + local architecture="$(to_lowercase "$1")" + + if [[ $architecture == \ ]]; then + machine_architecture="$(get_machine_architecture)" + if [[ "$machine_architecture" == "armv6-or-below" ]]; then + say_err "Architecture \`$machine_architecture\` not supported. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues" + return 1 + fi + + echo $machine_architecture + return 0 + fi + + case "$architecture" in + amd64|x64) + echo "x64" + return 0 + ;; + arm) + echo "arm" + return 0 + ;; + arm64) + echo "arm64" + return 0 + ;; + s390x) + echo "s390x" + return 0 + ;; + ppc64le) + echo "ppc64le" + return 0 + ;; + loongarch64) + echo "loongarch64" + return 0 + ;; + esac + + say_err "Architecture \`$architecture\` not supported. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues" + return 1 +} + +# args: +# version - $1 +# channel - $2 +# architecture - $3 +get_normalized_architecture_for_specific_sdk_version() { + eval $invocation + + local is_version_support_arm64="$(is_arm64_supported "$1")" + local is_channel_support_arm64="$(is_arm64_supported "$2")" + local architecture="$3"; + local osname="$(get_current_os_name)" + + if [ "$osname" == "osx" ] && [ "$architecture" == "arm64" ] && { [ "$is_version_support_arm64" = false ] || [ "$is_channel_support_arm64" = false ]; }; then + #check if rosetta is installed + if [ "$(/usr/bin/pgrep oahd >/dev/null 2>&1;echo $?)" -eq 0 ]; then + say_verbose "Changing user architecture from '$architecture' to 'x64' because .NET SDKs prior to version 6.0 do not support arm64." + echo "x64" + return 0; + else + say_err "Architecture \`$architecture\` is not supported for .NET SDK version \`$version\`. Please install Rosetta to allow emulation of the \`$architecture\` .NET SDK on this platform" + return 1 + fi + fi + + echo "$architecture" + return 0 +} + +# args: +# version or channel - $1 +is_arm64_supported() { + # Extract the major version by splitting on the dot + major_version="${1%%.*}" + + # Check if the major version is a valid number and less than 6 + case "$major_version" in + [0-9]*) + if [ "$major_version" -lt 6 ]; then + echo false + return 0 + fi + ;; + esac + + echo true + return 0 +} + +# args: +# user_defined_os - $1 +get_normalized_os() { + eval $invocation + + local osname="$(to_lowercase "$1")" + if [ ! -z "$osname" ]; then + case "$osname" in + osx | freebsd | rhel.6 | linux-musl | linux) + echo "$osname" + return 0 + ;; + macos) + osname='osx' + echo "$osname" + return 0 + ;; + *) + say_err "'$user_defined_os' is not a supported value for --os option, supported values are: osx, macos, linux, linux-musl, freebsd, rhel.6. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues." + return 1 + ;; + esac + else + osname="$(get_current_os_name)" || return 1 + fi + echo "$osname" + return 0 +} + +# args: +# quality - $1 +get_normalized_quality() { + eval $invocation + + local quality="$(to_lowercase "$1")" + if [ ! -z "$quality" ]; then + case "$quality" in + daily | preview) + echo "$quality" + return 0 + ;; + ga) + #ga quality is available without specifying quality, so normalizing it to empty + return 0 + ;; + *) + say_err "'$quality' is not a supported value for --quality option. Supported values are: daily, preview, ga. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues." + return 1 + ;; + esac + fi + return 0 +} + +# args: +# channel - $1 +get_normalized_channel() { + eval $invocation + + local channel="$(to_lowercase "$1")" + + if [[ $channel == current ]]; then + say_warning 'Value "Current" is deprecated for -Channel option. Use "STS" instead.' + fi + + if [[ $channel == release/* ]]; then + say_warning 'Using branch name with -Channel option is no longer supported with newer releases. Use -Quality option with a channel in X.Y format instead.'; + fi + + if [ ! -z "$channel" ]; then + case "$channel" in + lts) + echo "LTS" + return 0 + ;; + sts) + echo "STS" + return 0 + ;; + current) + echo "STS" + return 0 + ;; + *) + echo "$channel" + return 0 + ;; + esac + fi + + return 0 +} + +# args: +# runtime - $1 +get_normalized_product() { + eval $invocation + + local product="" + local runtime="$(to_lowercase "$1")" + if [[ "$runtime" == "dotnet" ]]; then + product="dotnet-runtime" + elif [[ "$runtime" == "aspnetcore" ]]; then + product="aspnetcore-runtime" + elif [ -z "$runtime" ]; then + product="dotnet-sdk" + fi + echo "$product" + return 0 +} + +# The version text returned from the feeds is a 1-line or 2-line string: +# For the SDK and the dotnet runtime (2 lines): +# Line 1: # commit_hash +# Line 2: # 4-part version +# For the aspnetcore runtime (1 line): +# Line 1: # 4-part version + +# args: +# version_text - stdin +get_version_from_latestversion_file_content() { + eval $invocation + + cat | tail -n 1 | sed 's/\r$//' + return 0 +} + +# args: +# install_root - $1 +# relative_path_to_package - $2 +# specific_version - $3 +is_dotnet_package_installed() { + eval $invocation + + local install_root="$1" + local relative_path_to_package="$2" + local specific_version="${3//[$'\t\r\n']}" + + local dotnet_package_path="$(combine_paths "$(combine_paths "$install_root" "$relative_path_to_package")" "$specific_version")" + say_verbose "is_dotnet_package_installed: dotnet_package_path=$dotnet_package_path" + + if [ -d "$dotnet_package_path" ]; then + return 0 + else + return 1 + fi +} + +# args: +# downloaded file - $1 +# remote_file_size - $2 +validate_remote_local_file_sizes() +{ + eval $invocation + + local downloaded_file="$1" + local remote_file_size="$2" + local file_size='' + + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + file_size="$(stat -c '%s' "$downloaded_file")" + elif [[ "$OSTYPE" == "darwin"* ]]; then + # hardcode in order to avoid conflicts with GNU stat + file_size="$(/usr/bin/stat -f '%z' "$downloaded_file")" + fi + + if [ -n "$file_size" ]; then + say "Downloaded file size is $file_size bytes." + + if [ -n "$remote_file_size" ] && [ -n "$file_size" ]; then + if [ "$remote_file_size" -ne "$file_size" ]; then + say "The remote and local file sizes are not equal. The remote file size is $remote_file_size bytes and the local size is $file_size bytes. The local package may be corrupted." + else + say "The remote and local file sizes are equal." + fi + fi + + else + say "Either downloaded or local package size can not be measured. One of them may be corrupted." + fi +} + +# args: +# azure_feed - $1 +# channel - $2 +# normalized_architecture - $3 +get_version_from_latestversion_file() { + eval $invocation + + local azure_feed="$1" + local channel="$2" + local normalized_architecture="$3" + + local version_file_url=null + if [[ "$runtime" == "dotnet" ]]; then + version_file_url="$azure_feed/Runtime/$channel/latest.version" + elif [[ "$runtime" == "aspnetcore" ]]; then + version_file_url="$azure_feed/aspnetcore/Runtime/$channel/latest.version" + elif [ -z "$runtime" ]; then + version_file_url="$azure_feed/Sdk/$channel/latest.version" + else + say_err "Invalid value for \$runtime" + return 1 + fi + say_verbose "get_version_from_latestversion_file: latest url: $version_file_url" + + download "$version_file_url" || return $? + return 0 +} + +# args: +# json_file - $1 +parse_globaljson_file_for_version() { + eval $invocation + + local json_file="$1" + if [ ! -f "$json_file" ]; then + say_err "Unable to find \`$json_file\`" + return 1 + fi + + sdk_section=$(cat "$json_file" | tr -d "\r" | awk '/"sdk"/,/}/') + if [ -z "$sdk_section" ]; then + say_err "Unable to parse the SDK node in \`$json_file\`" + return 1 + fi + + sdk_list=$(echo $sdk_section | awk -F"[{}]" '{print $2}') + sdk_list=${sdk_list//[\" ]/} + sdk_list=${sdk_list//,/$'\n'} + + local version_info="" + while read -r line; do + IFS=: + while read -r key value; do + if [[ "$key" == "version" ]]; then + version_info=$value + fi + done <<< "$line" + done <<< "$sdk_list" + if [ -z "$version_info" ]; then + say_err "Unable to find the SDK:version node in \`$json_file\`" + return 1 + fi + + unset IFS; + echo "$version_info" + return 0 +} + +# args: +# azure_feed - $1 +# channel - $2 +# normalized_architecture - $3 +# version - $4 +# json_file - $5 +get_specific_version_from_version() { + eval $invocation + + local azure_feed="$1" + local channel="$2" + local normalized_architecture="$3" + local version="$(to_lowercase "$4")" + local json_file="$5" + + if [ -z "$json_file" ]; then + if [[ "$version" == "latest" ]]; then + local version_info + version_info="$(get_version_from_latestversion_file "$azure_feed" "$channel" "$normalized_architecture" false)" || return 1 + say_verbose "get_specific_version_from_version: version_info=$version_info" + echo "$version_info" | get_version_from_latestversion_file_content + return 0 + else + echo "$version" + return 0 + fi + else + local version_info + version_info="$(parse_globaljson_file_for_version "$json_file")" || return 1 + echo "$version_info" + return 0 + fi +} + +# args: +# azure_feed - $1 +# channel - $2 +# normalized_architecture - $3 +# specific_version - $4 +# normalized_os - $5 +construct_download_link() { + eval $invocation + + local azure_feed="$1" + local channel="$2" + local normalized_architecture="$3" + local specific_version="${4//[$'\t\r\n']}" + local specific_product_version="$(get_specific_product_version "$1" "$4")" + local osname="$5" + + local download_link=null + if [[ "$runtime" == "dotnet" ]]; then + download_link="$azure_feed/Runtime/$specific_version/dotnet-runtime-$specific_product_version-$osname-$normalized_architecture.tar.gz" + elif [[ "$runtime" == "aspnetcore" ]]; then + download_link="$azure_feed/aspnetcore/Runtime/$specific_version/aspnetcore-runtime-$specific_product_version-$osname-$normalized_architecture.tar.gz" + elif [ -z "$runtime" ]; then + download_link="$azure_feed/Sdk/$specific_version/dotnet-sdk-$specific_product_version-$osname-$normalized_architecture.tar.gz" + else + return 1 + fi + + echo "$download_link" + return 0 +} + +# args: +# azure_feed - $1 +# specific_version - $2 +# download link - $3 (optional) +get_specific_product_version() { + # If we find a 'productVersion.txt' at the root of any folder, we'll use its contents + # to resolve the version of what's in the folder, superseding the specified version. + # if 'productVersion.txt' is missing but download link is already available, product version will be taken from download link + eval $invocation + + local azure_feed="$1" + local specific_version="${2//[$'\t\r\n']}" + local package_download_link="" + if [ $# -gt 2 ]; then + local package_download_link="$3" + fi + local specific_product_version=null + + # Try to get the version number, using the productVersion.txt file located next to the installer file. + local download_links=($(get_specific_product_version_url "$azure_feed" "$specific_version" true "$package_download_link") + $(get_specific_product_version_url "$azure_feed" "$specific_version" false "$package_download_link")) + + for download_link in "${download_links[@]}" + do + say_verbose "Checking for the existence of $download_link" + + if machine_has "curl" + then + if ! specific_product_version=$(curl -s --fail "${download_link}${feed_credential}" 2>&1); then + continue + else + echo "${specific_product_version//[$'\t\r\n']}" + return 0 + fi + + elif machine_has "wget" + then + specific_product_version=$(wget -qO- "${download_link}${feed_credential}" 2>&1) + if [ $? = 0 ]; then + echo "${specific_product_version//[$'\t\r\n']}" + return 0 + fi + fi + done + + # Getting the version number with productVersion.txt has failed. Try parsing the download link for a version number. + say_verbose "Failed to get the version using productVersion.txt file. Download link will be parsed instead." + specific_product_version="$(get_product_specific_version_from_download_link "$package_download_link" "$specific_version")" + echo "${specific_product_version//[$'\t\r\n']}" + return 0 +} + +# args: +# azure_feed - $1 +# specific_version - $2 +# is_flattened - $3 +# download link - $4 (optional) +get_specific_product_version_url() { + eval $invocation + + local azure_feed="$1" + local specific_version="$2" + local is_flattened="$3" + local package_download_link="" + if [ $# -gt 3 ]; then + local package_download_link="$4" + fi + + local pvFileName="productVersion.txt" + if [ "$is_flattened" = true ]; then + if [ -z "$runtime" ]; then + pvFileName="sdk-productVersion.txt" + elif [[ "$runtime" == "dotnet" ]]; then + pvFileName="runtime-productVersion.txt" + else + pvFileName="$runtime-productVersion.txt" + fi + fi + + local download_link=null + + if [ -z "$package_download_link" ]; then + if [[ "$runtime" == "dotnet" ]]; then + download_link="$azure_feed/Runtime/$specific_version/${pvFileName}" + elif [[ "$runtime" == "aspnetcore" ]]; then + download_link="$azure_feed/aspnetcore/Runtime/$specific_version/${pvFileName}" + elif [ -z "$runtime" ]; then + download_link="$azure_feed/Sdk/$specific_version/${pvFileName}" + else + return 1 + fi + else + download_link="${package_download_link%/*}/${pvFileName}" + fi + + say_verbose "Constructed productVersion link: $download_link" + echo "$download_link" + return 0 +} + +# args: +# download link - $1 +# specific version - $2 +get_product_specific_version_from_download_link() +{ + eval $invocation + + local download_link="$1" + local specific_version="$2" + local specific_product_version="" + + if [ -z "$download_link" ]; then + echo "$specific_version" + return 0 + fi + + #get filename + filename="${download_link##*/}" + + #product specific version follows the product name + #for filename 'dotnet-sdk-3.1.404-linux-x64.tar.gz': the product version is 3.1.404 + IFS='-' + read -ra filename_elems <<< "$filename" + count=${#filename_elems[@]} + if [[ "$count" -gt 2 ]]; then + specific_product_version="${filename_elems[2]}" + else + specific_product_version=$specific_version + fi + unset IFS; + echo "$specific_product_version" + return 0 +} + +# args: +# azure_feed - $1 +# channel - $2 +# normalized_architecture - $3 +# specific_version - $4 +construct_legacy_download_link() { + eval $invocation + + local azure_feed="$1" + local channel="$2" + local normalized_architecture="$3" + local specific_version="${4//[$'\t\r\n']}" + + local distro_specific_osname + distro_specific_osname="$(get_legacy_os_name)" || return 1 + + local legacy_download_link=null + if [[ "$runtime" == "dotnet" ]]; then + legacy_download_link="$azure_feed/Runtime/$specific_version/dotnet-$distro_specific_osname-$normalized_architecture.$specific_version.tar.gz" + elif [ -z "$runtime" ]; then + legacy_download_link="$azure_feed/Sdk/$specific_version/dotnet-dev-$distro_specific_osname-$normalized_architecture.$specific_version.tar.gz" + else + return 1 + fi + + echo "$legacy_download_link" + return 0 +} + +get_user_install_path() { + eval $invocation + + if [ ! -z "${DOTNET_INSTALL_DIR:-}" ]; then + echo "$DOTNET_INSTALL_DIR" + else + echo "$HOME/.dotnet" + fi + return 0 +} + +# args: +# install_dir - $1 +resolve_installation_path() { + eval $invocation + + local install_dir=$1 + if [ "$install_dir" = "" ]; then + local user_install_path="$(get_user_install_path)" + say_verbose "resolve_installation_path: user_install_path=$user_install_path" + echo "$user_install_path" + return 0 + fi + + echo "$install_dir" + return 0 +} + +# args: +# relative_or_absolute_path - $1 +get_absolute_path() { + eval $invocation + + local relative_or_absolute_path=$1 + echo "$(cd "$(dirname "$1")" && pwd -P)/$(basename "$1")" + return 0 +} + +# args: +# override - $1 (boolean, true or false) +get_cp_options() { + eval $invocation + + local override="$1" + local override_switch="" + + if [ "$override" = false ]; then + override_switch="-n" + + # create temporary files to check if 'cp -u' is supported + tmp_dir="$(mktemp -d)" + tmp_file="$tmp_dir/testfile" + tmp_file2="$tmp_dir/testfile2" + + touch "$tmp_file" + + # use -u instead of -n if it's available + if cp -u "$tmp_file" "$tmp_file2" 2>/dev/null; then + override_switch="-u" + fi + + # clean up + rm -f "$tmp_file" "$tmp_file2" + rm -rf "$tmp_dir" + fi + + echo "$override_switch" +} + +# args: +# input_files - stdin +# root_path - $1 +# out_path - $2 +# override - $3 +copy_files_or_dirs_from_list() { + eval $invocation + + local root_path="$(remove_trailing_slash "$1")" + local out_path="$(remove_trailing_slash "$2")" + local override="$3" + local override_switch="$(get_cp_options "$override")" + + cat | uniq | while read -r file_path; do + local path="$(remove_beginning_slash "${file_path#$root_path}")" + local target="$out_path/$path" + if [ "$override" = true ] || (! ([ -d "$target" ] || [ -e "$target" ])); then + mkdir -p "$out_path/$(dirname "$path")" + if [ -d "$target" ]; then + rm -rf "$target" + fi + cp -R $override_switch "$root_path/$path" "$target" + fi + done +} + +# args: +# zip_uri - $1 +get_remote_file_size() { + local zip_uri="$1" + + if machine_has "curl"; then + file_size=$(curl -sI "$zip_uri" | grep -i content-length | awk '{ num = $2 + 0; print num }') + elif machine_has "wget"; then + file_size=$(wget --spider --server-response -O /dev/null "$zip_uri" 2>&1 | grep -i 'Content-Length:' | awk '{ num = $2 + 0; print num }') + else + say "Neither curl nor wget is available on this system." + return + fi + + if [ -n "$file_size" ]; then + say "Remote file $zip_uri size is $file_size bytes." + echo "$file_size" + else + say_verbose "Content-Length header was not extracted for $zip_uri." + echo "" + fi +} + +# args: +# zip_path - $1 +# out_path - $2 +# remote_file_size - $3 +extract_dotnet_package() { + eval $invocation + + local zip_path="$1" + local out_path="$2" + local remote_file_size="$3" + + local temp_out_path="$(mktemp -d "$temporary_file_template")" + + local failed=false + tar -xzf "$zip_path" -C "$temp_out_path" > /dev/null || failed=true + + local folders_with_version_regex='^.*/[0-9]+\.[0-9]+[^/]+/' + find "$temp_out_path" -type f | grep -Eo "$folders_with_version_regex" | sort | copy_files_or_dirs_from_list "$temp_out_path" "$out_path" false + find "$temp_out_path" -type f | grep -Ev "$folders_with_version_regex" | copy_files_or_dirs_from_list "$temp_out_path" "$out_path" "$override_non_versioned_files" + + validate_remote_local_file_sizes "$zip_path" "$remote_file_size" + + rm -rf "$temp_out_path" + if [ -z ${keep_zip+x} ]; then + rm -f "$zip_path" && say_verbose "Temporary archive file $zip_path was removed" + fi + + if [ "$failed" = true ]; then + say_err "Extraction failed" + return 1 + fi + return 0 +} + +# args: +# remote_path - $1 +# disable_feed_credential - $2 +get_http_header() +{ + eval $invocation + local remote_path="$1" + local disable_feed_credential="$2" + + local failed=false + local response + if machine_has "curl"; then + get_http_header_curl $remote_path $disable_feed_credential || failed=true + elif machine_has "wget"; then + get_http_header_wget $remote_path $disable_feed_credential || failed=true + else + failed=true + fi + if [ "$failed" = true ]; then + say_verbose "Failed to get HTTP header: '$remote_path'." + return 1 + fi + return 0 +} + +# args: +# remote_path - $1 +# disable_feed_credential - $2 +get_http_header_curl() { + eval $invocation + local remote_path="$1" + local disable_feed_credential="$2" + + remote_path_with_credential="$remote_path" + if [ "$disable_feed_credential" = false ]; then + remote_path_with_credential+="$feed_credential" + fi + + curl_options="-I -sSL --retry 5 --retry-delay 2 --connect-timeout 15 " + curl $curl_options "$remote_path_with_credential" 2>&1 || return 1 + return 0 +} + +# args: +# remote_path - $1 +# disable_feed_credential - $2 +get_http_header_wget() { + eval $invocation + local remote_path="$1" + local disable_feed_credential="$2" + local wget_options="-q -S --spider --tries 5 " + + local wget_options_extra='' + + # Test for options that aren't supported on all wget implementations. + if [[ $(wget -h 2>&1 | grep -E 'waitretry|connect-timeout') ]]; then + wget_options_extra="--waitretry 2 --connect-timeout 15 " + else + say "wget extra options are unavailable for this environment" + fi + + remote_path_with_credential="$remote_path" + if [ "$disable_feed_credential" = false ]; then + remote_path_with_credential+="$feed_credential" + fi + + wget $wget_options $wget_options_extra "$remote_path_with_credential" 2>&1 + + return $? +} + +# args: +# remote_path - $1 +# [out_path] - $2 - stdout if not provided +download() { + eval $invocation + + local remote_path="$1" + local out_path="${2:-}" + + if [[ "$remote_path" != "http"* ]]; then + cp "$remote_path" "$out_path" + return $? + fi + + local failed=false + local attempts=0 + while [ $attempts -lt 3 ]; do + attempts=$((attempts+1)) + failed=false + if machine_has "curl"; then + downloadcurl "$remote_path" "$out_path" || failed=true + elif machine_has "wget"; then + downloadwget "$remote_path" "$out_path" || failed=true + else + say_err "Missing dependency: neither curl nor wget was found." + exit 1 + fi + + if [ "$failed" = false ] || [ $attempts -ge 3 ] || { [ -n "${http_code-}" ] && [ "${http_code}" = "404" ]; }; then + break + fi + + say "Download attempt #$attempts has failed: ${http_code-} ${download_error_msg-}" + say "Attempt #$((attempts+1)) will start in $((attempts*10)) seconds." + sleep $((attempts*10)) + done + + if [ "$failed" = true ]; then + say_verbose "Download failed: $remote_path" + return 1 + fi + return 0 +} + +# Updates global variables $http_code and $download_error_msg +downloadcurl() { + eval $invocation + unset http_code + unset download_error_msg + local remote_path="$1" + local out_path="${2:-}" + # Append feed_credential as late as possible before calling curl to avoid logging feed_credential + # Avoid passing URI with credentials to functions: note, most of them echoing parameters of invocation in verbose output. + local remote_path_with_credential="${remote_path}${feed_credential}" + local curl_options="--retry 20 --retry-delay 2 --connect-timeout 15 -sSL -f --create-dirs " + local curl_exit_code=0; + if [ -z "$out_path" ]; then + curl_output=$(curl $curl_options "$remote_path_with_credential" 2>&1) + curl_exit_code=$? + echo "$curl_output" + else + curl_output=$(curl $curl_options -o "$out_path" "$remote_path_with_credential" 2>&1) + curl_exit_code=$? + fi + + # Regression in curl causes curl with --retry to return a 0 exit code even when it fails to download a file - https://github.com/curl/curl/issues/17554 + if [ $curl_exit_code -eq 0 ] && echo "$curl_output" | grep -q "^curl: ([0-9]*) "; then + curl_exit_code=$(echo "$curl_output" | sed 's/curl: (\([0-9]*\)).*/\1/') + fi + + if [ $curl_exit_code -gt 0 ]; then + download_error_msg="Unable to download $remote_path." + # Check for curl timeout codes + if [[ $curl_exit_code == 7 || $curl_exit_code == 28 ]]; then + download_error_msg+=" Failed to reach the server: connection timeout." + else + local disable_feed_credential=false + local response=$(get_http_header_curl $remote_path $disable_feed_credential) + http_code=$( echo "$response" | awk '/^HTTP/{print $2}' | tail -1 ) + if [[ ! -z $http_code && $http_code != 2* ]]; then + download_error_msg+=" Returned HTTP status code: $http_code." + fi + fi + say_verbose "$download_error_msg" + return 1 + fi + return 0 +} + + +# Updates global variables $http_code and $download_error_msg +downloadwget() { + eval $invocation + unset http_code + unset download_error_msg + local remote_path="$1" + local out_path="${2:-}" + # Append feed_credential as late as possible before calling wget to avoid logging feed_credential + local remote_path_with_credential="${remote_path}${feed_credential}" + local wget_options="--tries 20 " + + local wget_options_extra='' + local wget_result='' + + # Test for options that aren't supported on all wget implementations. + if [[ $(wget -h 2>&1 | grep -E 'waitretry|connect-timeout') ]]; then + wget_options_extra="--waitretry 2 --connect-timeout 15 " + else + say "wget extra options are unavailable for this environment" + fi + + if [ -z "$out_path" ]; then + wget -q $wget_options $wget_options_extra -O - "$remote_path_with_credential" 2>&1 + wget_result=$? + else + wget $wget_options $wget_options_extra -O "$out_path" "$remote_path_with_credential" 2>&1 + wget_result=$? + fi + + if [[ $wget_result != 0 ]]; then + local disable_feed_credential=false + local response=$(get_http_header_wget $remote_path $disable_feed_credential) + http_code=$( echo "$response" | awk '/^ HTTP/{print $2}' | tail -1 ) + download_error_msg="Unable to download $remote_path." + if [[ ! -z $http_code && $http_code != 2* ]]; then + download_error_msg+=" Returned HTTP status code: $http_code." + # wget exit code 4 stands for network-issue + elif [[ $wget_result == 4 ]]; then + download_error_msg+=" Failed to reach the server: connection timeout." + fi + say_verbose "$download_error_msg" + return 1 + fi + + return 0 +} + +get_download_link_from_aka_ms() { + eval $invocation + + #quality is not supported for LTS or STS channel + #STS maps to current + if [[ ! -z "$normalized_quality" && ("$normalized_channel" == "LTS" || "$normalized_channel" == "STS") ]]; then + normalized_quality="" + say_warning "Specifying quality for STS or LTS channel is not supported, the quality will be ignored." + fi + + say_verbose "Retrieving primary payload URL from aka.ms for channel: '$normalized_channel', quality: '$normalized_quality', product: '$normalized_product', os: '$normalized_os', architecture: '$normalized_architecture'." + + #construct aka.ms link + aka_ms_link="https://aka.ms/dotnet" + if [ "$internal" = true ]; then + aka_ms_link="$aka_ms_link/internal" + fi + aka_ms_link="$aka_ms_link/$normalized_channel" + if [[ ! -z "$normalized_quality" ]]; then + aka_ms_link="$aka_ms_link/$normalized_quality" + fi + aka_ms_link="$aka_ms_link/$normalized_product-$normalized_os-$normalized_architecture.tar.gz" + say_verbose "Constructed aka.ms link: '$aka_ms_link'." + + #get HTTP response + #do not pass credentials as a part of the $aka_ms_link and do not apply credentials in the get_http_header function + #otherwise the redirect link would have credentials as well + #it would result in applying credentials twice to the resulting link and thus breaking it, and in echoing credentials to the output as a part of redirect link + disable_feed_credential=true + response="$(get_http_header $aka_ms_link $disable_feed_credential)" + + say_verbose "Received response: $response" + # Get results of all the redirects. + http_codes=$( echo "$response" | awk '$1 ~ /^HTTP/ {print $2}' ) + # They all need to be 301, otherwise some links are broken (except for the last, which is not a redirect but 200 or 404). + broken_redirects=$( echo "$http_codes" | sed '$d' | grep -v '301' ) + # The response may end without final code 2xx/4xx/5xx somehow, e.g. network restrictions on www.bing.com causes redirecting to bing.com fails with connection refused. + # In this case it should not exclude the last. + last_http_code=$( echo "$http_codes" | tail -n 1 ) + if ! [[ $last_http_code =~ ^(2|4|5)[0-9][0-9]$ ]]; then + broken_redirects=$( echo "$http_codes" | grep -v '301' ) + fi + + # All HTTP codes are 301 (Moved Permanently), the redirect link exists. + if [[ -z "$broken_redirects" ]]; then + aka_ms_download_link=$( echo "$response" | awk '$1 ~ /^Location/{print $2}' | tail -1 | tr -d '\r') + + if [[ -z "$aka_ms_download_link" ]]; then + say_verbose "The aka.ms link '$aka_ms_link' is not valid: failed to get redirect location." + return 1 + fi + + say_verbose "The redirect location retrieved: '$aka_ms_download_link'." + return 0 + else + say_verbose "The aka.ms link '$aka_ms_link' is not valid: received HTTP code: $(echo "$broken_redirects" | paste -sd "," -)." + return 1 + fi +} + +get_feeds_to_use() +{ + feeds=( + "https://builds.dotnet.microsoft.com/dotnet" + "https://ci.dot.net/public" + ) + + if [[ -n "$azure_feed" ]]; then + feeds=("$azure_feed") + fi + + if [[ -n "$uncached_feed" ]]; then + feeds=("$uncached_feed") + fi +} + +# THIS FUNCTION MAY EXIT (if the determined version is already installed). +generate_download_links() { + + download_links=() + specific_versions=() + effective_versions=() + link_types=() + + # If generate_akams_links returns false, no fallback to old links. Just terminate. + # This function may also 'exit' (if the determined version is already installed). + generate_akams_links || return + + # Check other feeds only if we haven't been able to find an aka.ms link. + if [[ "${#download_links[@]}" -lt 1 ]]; then + for feed in ${feeds[@]} + do + # generate_regular_links may also 'exit' (if the determined version is already installed). + generate_regular_links $feed || return + done + fi + + if [[ "${#download_links[@]}" -eq 0 ]]; then + say_err "Failed to resolve the exact version number." + return 1 + fi + + say_verbose "Generated ${#download_links[@]} links." + for link_index in ${!download_links[@]} + do + say_verbose "Link $link_index: ${link_types[$link_index]}, ${effective_versions[$link_index]}, ${download_links[$link_index]}" + done +} + +# THIS FUNCTION MAY EXIT (if the determined version is already installed). +generate_akams_links() { + local valid_aka_ms_link=true; + + normalized_version="$(to_lowercase "$version")" + if [[ "$normalized_version" != "latest" ]] && [ -n "$normalized_quality" ]; then + say_err "Quality and Version options are not allowed to be specified simultaneously. See https://learn.microsoft.com/dotnet/core/tools/dotnet-install-script#options for details." + return 1 + fi + + if [[ -n "$json_file" || "$normalized_version" != "latest" ]]; then + # aka.ms links are not needed when exact version is specified via command or json file + return + fi + + get_download_link_from_aka_ms || valid_aka_ms_link=false + + if [[ "$valid_aka_ms_link" == true ]]; then + say_verbose "Retrieved primary payload URL from aka.ms link: '$aka_ms_download_link'." + say_verbose "Downloading using legacy url will not be attempted." + + download_link=$aka_ms_download_link + + #get version from the path + IFS='/' + read -ra pathElems <<< "$download_link" + count=${#pathElems[@]} + specific_version="${pathElems[count-2]}" + unset IFS; + say_verbose "Version: '$specific_version'." + + #Retrieve effective version + effective_version="$(get_specific_product_version "$azure_feed" "$specific_version" "$download_link")" + + # Add link info to arrays + download_links+=($download_link) + specific_versions+=($specific_version) + effective_versions+=($effective_version) + link_types+=("aka.ms") + + # Check if the SDK version is already installed. + if [[ "$dry_run" != true ]] && is_dotnet_package_installed "$install_root" "$asset_relative_path" "$effective_version"; then + say "$asset_name with version '$effective_version' is already installed." + exit 0 + fi + + return 0 + fi + + # if quality is specified - exit with error - there is no fallback approach + if [ ! -z "$normalized_quality" ]; then + say_err "Failed to locate the latest version in the channel '$normalized_channel' with '$normalized_quality' quality for '$normalized_product', os: '$normalized_os', architecture: '$normalized_architecture'." + say_err "Refer to: https://aka.ms/dotnet-os-lifecycle for information on .NET Core support." + return 1 + fi + say_verbose "Falling back to latest.version file approach." +} + +# THIS FUNCTION MAY EXIT (if the determined version is already installed) +# args: +# feed - $1 +generate_regular_links() { + local feed="$1" + local valid_legacy_download_link=true + + specific_version=$(get_specific_version_from_version "$feed" "$channel" "$normalized_architecture" "$version" "$json_file") || specific_version='0' + + if [[ "$specific_version" == '0' ]]; then + say_verbose "Failed to resolve the specific version number using feed '$feed'" + return + fi + + effective_version="$(get_specific_product_version "$feed" "$specific_version")" + say_verbose "specific_version=$specific_version" + + download_link="$(construct_download_link "$feed" "$channel" "$normalized_architecture" "$specific_version" "$normalized_os")" + say_verbose "Constructed primary named payload URL: $download_link" + + # Add link info to arrays + download_links+=($download_link) + specific_versions+=($specific_version) + effective_versions+=($effective_version) + link_types+=("primary") + + legacy_download_link="$(construct_legacy_download_link "$feed" "$channel" "$normalized_architecture" "$specific_version")" || valid_legacy_download_link=false + + if [ "$valid_legacy_download_link" = true ]; then + say_verbose "Constructed legacy named payload URL: $legacy_download_link" + + download_links+=($legacy_download_link) + specific_versions+=($specific_version) + effective_versions+=($effective_version) + link_types+=("legacy") + else + legacy_download_link="" + say_verbose "Could not construct a legacy_download_link; omitting..." + fi + + # Check if the SDK version is already installed. + if [[ "$dry_run" != true ]] && is_dotnet_package_installed "$install_root" "$asset_relative_path" "$effective_version"; then + say "$asset_name with version '$effective_version' is already installed." + exit 0 + fi +} + +print_dry_run() { + + say "Payload URLs:" + + for link_index in "${!download_links[@]}" + do + say "URL #$link_index - ${link_types[$link_index]}: ${download_links[$link_index]}" + done + + resolved_version=${specific_versions[0]} + repeatable_command="./$script_name --version "\""$resolved_version"\"" --install-dir "\""$install_root"\"" --architecture "\""$normalized_architecture"\"" --os "\""$normalized_os"\""" + + if [ ! -z "$normalized_quality" ]; then + repeatable_command+=" --quality "\""$normalized_quality"\""" + fi + + if [[ "$runtime" == "dotnet" ]]; then + repeatable_command+=" --runtime "\""dotnet"\""" + elif [[ "$runtime" == "aspnetcore" ]]; then + repeatable_command+=" --runtime "\""aspnetcore"\""" + fi + + repeatable_command+="$non_dynamic_parameters" + + if [ -n "$feed_credential" ]; then + repeatable_command+=" --feed-credential "\"""\""" + fi + + say "Repeatable invocation: $repeatable_command" +} + +calculate_vars() { + eval $invocation + + script_name=$(basename "$0") + normalized_architecture="$(get_normalized_architecture_from_architecture "$architecture")" + say_verbose "Normalized architecture: '$normalized_architecture'." + normalized_os="$(get_normalized_os "$user_defined_os")" + say_verbose "Normalized OS: '$normalized_os'." + normalized_quality="$(get_normalized_quality "$quality")" + say_verbose "Normalized quality: '$normalized_quality'." + normalized_channel="$(get_normalized_channel "$channel")" + say_verbose "Normalized channel: '$normalized_channel'." + normalized_product="$(get_normalized_product "$runtime")" + say_verbose "Normalized product: '$normalized_product'." + install_root="$(resolve_installation_path "$install_dir")" + say_verbose "InstallRoot: '$install_root'." + + normalized_architecture="$(get_normalized_architecture_for_specific_sdk_version "$version" "$normalized_channel" "$normalized_architecture")" + + if [[ "$runtime" == "dotnet" ]]; then + asset_relative_path="shared/Microsoft.NETCore.App" + asset_name=".NET Core Runtime" + elif [[ "$runtime" == "aspnetcore" ]]; then + asset_relative_path="shared/Microsoft.AspNetCore.App" + asset_name="ASP.NET Core Runtime" + elif [ -z "$runtime" ]; then + asset_relative_path="sdk" + asset_name=".NET Core SDK" + fi + + get_feeds_to_use +} + +install_dotnet() { + eval $invocation + local download_failed=false + local download_completed=false + local remote_file_size=0 + + mkdir -p "$install_root" + zip_path="${zip_path:-$(mktemp "$temporary_file_template")}" + say_verbose "Archive path: $zip_path" + + for link_index in "${!download_links[@]}" + do + download_link="${download_links[$link_index]}" + specific_version="${specific_versions[$link_index]}" + effective_version="${effective_versions[$link_index]}" + link_type="${link_types[$link_index]}" + + say "Attempting to download using $link_type link $download_link" + + # The download function will set variables $http_code and $download_error_msg in case of failure. + download_failed=false + download "$download_link" "$zip_path" 2>&1 || download_failed=true + + if [ "$download_failed" = true ]; then + case ${http_code-} in + 404) + say "The resource at $link_type link '$download_link' is not available." + ;; + *) + say "Failed to download $link_type link '$download_link': ${http_code-} ${download_error_msg-}" + ;; + esac + rm -f "$zip_path" 2>&1 && say_verbose "Temporary archive file $zip_path was removed" + else + download_completed=true + break + fi + done + + if [[ "$download_completed" == false ]]; then + say_err "Could not find \`$asset_name\` with version = $specific_version" + say_err "Refer to: https://aka.ms/dotnet-os-lifecycle for information on .NET Core support" + return 1 + fi + + remote_file_size="$(get_remote_file_size "$download_link")" + + say "Extracting archive from $download_link" + extract_dotnet_package "$zip_path" "$install_root" "$remote_file_size" || return 1 + + # Check if the SDK version is installed; if not, fail the installation. + # if the version contains "RTM" or "servicing"; check if a 'release-type' SDK version is installed. + if [[ $specific_version == *"rtm"* || $specific_version == *"servicing"* ]]; then + IFS='-' + read -ra verArr <<< "$specific_version" + release_version="${verArr[0]}" + unset IFS; + say_verbose "Checking installation: version = $release_version" + if is_dotnet_package_installed "$install_root" "$asset_relative_path" "$release_version"; then + say "Installed version is $effective_version" + return 0 + fi + fi + + # Check if the standard SDK version is installed. + say_verbose "Checking installation: version = $effective_version" + if is_dotnet_package_installed "$install_root" "$asset_relative_path" "$effective_version"; then + say "Installed version is $effective_version" + return 0 + fi + + # Version verification failed. More likely something is wrong either with the downloaded content or with the verification algorithm. + say_err "Failed to verify the version of installed \`$asset_name\`.\nInstallation source: $download_link.\nInstallation location: $install_root.\nReport the bug at https://github.com/dotnet/install-scripts/issues." + say_err "\`$asset_name\` with version = $effective_version failed to install with an error." + return 1 +} + +args=("$@") + +local_version_file_relative_path="/.version" +bin_folder_relative_path="" +temporary_file_template="${TMPDIR:-/tmp}/dotnet.XXXXXXXXX" + +channel="LTS" +version="Latest" +json_file="" +install_dir="" +architecture="" +dry_run=false +no_path=false +azure_feed="" +uncached_feed="" +feed_credential="" +verbose=false +runtime="" +runtime_id="" +quality="" +internal=false +override_non_versioned_files=true +non_dynamic_parameters="" +user_defined_os="" + +while [ $# -ne 0 ] +do + name="$1" + case "$name" in + -c|--channel|-[Cc]hannel) + shift + channel="$1" + ;; + -v|--version|-[Vv]ersion) + shift + version="$1" + ;; + -q|--quality|-[Qq]uality) + shift + quality="$1" + ;; + --internal|-[Ii]nternal) + internal=true + non_dynamic_parameters+=" $name" + ;; + -i|--install-dir|-[Ii]nstall[Dd]ir) + shift + install_dir="$1" + ;; + --arch|--architecture|-[Aa]rch|-[Aa]rchitecture) + shift + architecture="$1" + ;; + --os|-[Oo][SS]) + shift + user_defined_os="$1" + ;; + --shared-runtime|-[Ss]hared[Rr]untime) + say_warning "The --shared-runtime flag is obsolete and may be removed in a future version of this script. The recommended usage is to specify '--runtime dotnet'." + if [ -z "$runtime" ]; then + runtime="dotnet" + fi + ;; + --runtime|-[Rr]untime) + shift + runtime="$1" + if [[ "$runtime" != "dotnet" ]] && [[ "$runtime" != "aspnetcore" ]]; then + say_err "Unsupported value for --runtime: '$1'. Valid values are 'dotnet' and 'aspnetcore'." + if [[ "$runtime" == "windowsdesktop" ]]; then + say_err "WindowsDesktop archives are manufactured for Windows platforms only." + fi + exit 1 + fi + ;; + --dry-run|-[Dd]ry[Rr]un) + dry_run=true + ;; + --no-path|-[Nn]o[Pp]ath) + no_path=true + non_dynamic_parameters+=" $name" + ;; + --verbose|-[Vv]erbose) + verbose=true + non_dynamic_parameters+=" $name" + ;; + --azure-feed|-[Aa]zure[Ff]eed) + shift + azure_feed="$1" + non_dynamic_parameters+=" $name "\""$1"\""" + ;; + --uncached-feed|-[Uu]ncached[Ff]eed) + shift + uncached_feed="$1" + non_dynamic_parameters+=" $name "\""$1"\""" + ;; + --feed-credential|-[Ff]eed[Cc]redential) + shift + feed_credential="$1" + #feed_credential should start with "?", for it to be added to the end of the link. + #adding "?" at the beginning of the feed_credential if needed. + [[ -z "$(echo $feed_credential)" ]] || [[ $feed_credential == \?* ]] || feed_credential="?$feed_credential" + ;; + --runtime-id|-[Rr]untime[Ii]d) + shift + runtime_id="$1" + non_dynamic_parameters+=" $name "\""$1"\""" + say_warning "Use of --runtime-id is obsolete and should be limited to the versions below 2.1. To override architecture, use --architecture option instead. To override OS, use --os option instead." + ;; + --jsonfile|-[Jj][Ss]on[Ff]ile) + shift + json_file="$1" + ;; + --skip-non-versioned-files|-[Ss]kip[Nn]on[Vv]ersioned[Ff]iles) + override_non_versioned_files=false + non_dynamic_parameters+=" $name" + ;; + --keep-zip|-[Kk]eep[Zz]ip) + keep_zip=true + non_dynamic_parameters+=" $name" + ;; + --zip-path|-[Zz]ip[Pp]ath) + shift + zip_path="$1" + ;; + -?|--?|-h|--help|-[Hh]elp) + script_name="dotnet-install.sh" + echo ".NET Tools Installer" + echo "Usage:" + echo " # Install a .NET SDK of a given Quality from a given Channel" + echo " $script_name [-c|--channel ] [-q|--quality ]" + echo " # Install a .NET SDK of a specific public version" + echo " $script_name [-v|--version ]" + echo " $script_name -h|-?|--help" + echo "" + echo "$script_name is a simple command line interface for obtaining dotnet cli." + echo " Note that the intended use of this script is for Continuous Integration (CI) scenarios, where:" + echo " - The SDK needs to be installed without user interaction and without admin rights." + echo " - The SDK installation doesn't need to persist across multiple CI runs." + echo " To set up a development environment or to run apps, use installers rather than this script. Visit https://dotnet.microsoft.com/download to get the installer." + echo "" + echo "Options:" + echo " -c,--channel Download from the channel specified, Defaults to \`$channel\`." + echo " -Channel" + echo " Possible values:" + echo " - STS - the most recent Standard Term Support release" + echo " - LTS - the most recent Long Term Support release" + echo " - 2-part version in a format A.B - represents a specific release" + echo " examples: 2.0; 1.0" + echo " - 3-part version in a format A.B.Cxx - represents a specific SDK release" + echo " examples: 5.0.1xx, 5.0.2xx." + echo " Supported since 5.0 release" + echo " Warning: Value 'Current' is deprecated for the Channel parameter. Use 'STS' instead." + echo " Note: The version parameter overrides the channel parameter when any version other than 'latest' is used." + echo " -v,--version Use specific VERSION, Defaults to \`$version\`." + echo " -Version" + echo " Possible values:" + echo " - latest - the latest build on specific channel" + echo " - 3-part version in a format A.B.C - represents specific version of build" + echo " examples: 2.0.0-preview2-006120; 1.1.0" + echo " -q,--quality Download the latest build of specified quality in the channel." + echo " -Quality" + echo " The possible values are: daily, preview, GA." + echo " Works only in combination with channel. Not applicable for STS and LTS channels and will be ignored if those channels are used." + echo " For SDK use channel in A.B.Cxx format. Using quality for SDK together with channel in A.B format is not supported." + echo " Supported since 5.0 release." + echo " Note: The version parameter overrides the channel parameter when any version other than 'latest' is used, and therefore overrides the quality." + echo " --internal,-Internal Download internal builds. Requires providing credentials via --feed-credential parameter." + echo " --feed-credential Token to access Azure feed. Used as a query string to append to the Azure feed." + echo " -FeedCredential This parameter typically is not specified." + echo " -i,--install-dir Install under specified location (see Install Location below)" + echo " -InstallDir" + echo " --architecture Architecture of dotnet binaries to be installed, Defaults to \`$architecture\`." + echo " --arch,-Architecture,-Arch" + echo " Possible values: x64, arm, arm64, s390x, ppc64le and loongarch64" + echo " --os Specifies operating system to be used when selecting the installer." + echo " Overrides the OS determination approach used by the script. Supported values: osx, linux, linux-musl, freebsd, rhel.6." + echo " In case any other value is provided, the platform will be determined by the script based on machine configuration." + echo " Not supported for legacy links. Use --runtime-id to specify platform for legacy links." + echo " Refer to: https://aka.ms/dotnet-os-lifecycle for more information." + echo " --runtime Installs a shared runtime only, without the SDK." + echo " -Runtime" + echo " Possible values:" + echo " - dotnet - the Microsoft.NETCore.App shared runtime" + echo " - aspnetcore - the Microsoft.AspNetCore.App shared runtime" + echo " --dry-run,-DryRun Do not perform installation. Display download link." + echo " --no-path, -NoPath Do not set PATH for the current process." + echo " --verbose,-Verbose Display diagnostics information." + echo " --azure-feed,-AzureFeed For internal use only." + echo " Allows using a different storage to download SDK archives from." + echo " --uncached-feed,-UncachedFeed For internal use only." + echo " Allows using a different storage to download SDK archives from." + echo " --skip-non-versioned-files Skips non-versioned files if they already exist, such as the dotnet executable." + echo " -SkipNonVersionedFiles" + echo " --jsonfile Determines the SDK version from a user specified global.json file." + echo " Note: global.json must have a value for 'SDK:Version'" + echo " --keep-zip,-KeepZip If set, downloaded file is kept." + echo " --zip-path, -ZipPath If set, downloaded file is stored at the specified path." + echo " -?,--?,-h,--help,-Help Shows this help message" + echo "" + echo "Install Location:" + echo " Location is chosen in following order:" + echo " - --install-dir option" + echo " - Environmental variable DOTNET_INSTALL_DIR" + echo " - $HOME/.dotnet" + exit 0 + ;; + *) + say_err "Unknown argument \`$name\`" + exit 1 + ;; + esac + + shift +done + +say_verbose "Note that the intended use of this script is for Continuous Integration (CI) scenarios, where:" +say_verbose "- The SDK needs to be installed without user interaction and without admin rights." +say_verbose "- The SDK installation doesn't need to persist across multiple CI runs." +say_verbose "To set up a development environment or to run apps, use installers rather than this script. Visit https://dotnet.microsoft.com/download to get the installer.\n" + +if [ "$internal" = true ] && [ -z "$(echo $feed_credential)" ]; then + message="Provide credentials via --feed-credential parameter." + if [ "$dry_run" = true ]; then + say_warning "$message" + else + say_err "$message" + exit 1 + fi +fi + +check_min_reqs +calculate_vars +# generate_regular_links call below will 'exit' if the determined version is already installed. +generate_download_links + +if [[ "$dry_run" = true ]]; then + print_dry_run + exit 0 +fi + +install_dotnet + +bin_path="$(get_absolute_path "$(combine_paths "$install_root" "$bin_folder_relative_path")")" +if [ "$no_path" = false ]; then + say "Adding to current process PATH: \`$bin_path\`. Note: This change will be visible only when sourcing script." + export PATH="$bin_path":"$PATH" +else + say "Binaries of dotnet can be found in $bin_path" +fi + +say "Note that the script does not resolve dependencies during installation." +say "To check the list of dependencies, go to https://learn.microsoft.com/dotnet/core/install, select your operating system and check the \"Dependencies\" section." +say "Installation finished successfully." diff --git a/tools/dotnet-test-cloud.ps1 b/tools/dotnet-test-cloud.ps1 new file mode 100644 index 000000000..a4dde3fd2 --- /dev/null +++ b/tools/dotnet-test-cloud.ps1 @@ -0,0 +1,141 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Runs tests as they are run in cloud test runs. +.PARAMETER Configuration + The configuration within which to run tests +.PARAMETER Agent + The name of the agent. This is used in preparing test run titles. +.PARAMETER PublishResults + A switch to publish results to Azure Pipelines. +.PARAMETER x86 + A switch to run the tests in an x86 process. +.PARAMETER dotnet32 + The path to a 32-bit dotnet executable to use. +#> +[CmdletBinding()] +Param( + [string]$Configuration='Debug', + [string]$Agent='Local', + [switch]$PublishResults, + [switch]$x86, + [string]$dotnet32 +) + +$RepoRoot = (Resolve-Path "$PSScriptRoot/..").Path +$ArtifactStagingFolder = & "$PSScriptRoot/Get-ArtifactsStagingDirectory.ps1" +$OnCI = ($env:CI -or $env:TF_BUILD) + +$dotnet = 'dotnet' +if ($x86) { + $x86RunTitleSuffix = ", x86" + if ($dotnet32) { + $dotnet = $dotnet32 + } else { + $dotnet32Possibilities = "$PSScriptRoot\../obj/tools/x86/.dotnet/dotnet.exe", "$env:AGENT_TOOLSDIRECTORY/x86/dotnet/dotnet.exe", "${env:ProgramFiles(x86)}\dotnet\dotnet.exe" + $dotnet32Matches = $dotnet32Possibilities |? { Test-Path $_ } + if ($dotnet32Matches) { + $dotnet = Resolve-Path @($dotnet32Matches)[0] + Write-Host "Running tests using `"$dotnet`"" -ForegroundColor DarkGray + } else { + Write-Error "Unable to find 32-bit dotnet.exe" + return 1 + } + } +} + +$testBinLog = Join-Path $ArtifactStagingFolder (Join-Path build_logs test.binlog) +$testLogs = Join-Path $ArtifactStagingFolder test_logs + +$extraArgs = @() +if ($IsLinux -or $IsMacOS) { + $extraArgs += '-p:Platform=NonWindows' +} + +$globalJson = Get-Content $PSScriptRoot/../global.json | ConvertFrom-Json +$isMTP = $globalJson.test.runner -eq 'Microsoft.Testing.Platform' +$extraArgs = @() +$failedTests = 0 + +if ($isMTP) { + if ($OnCI) { $extraArgs += '--no-progress' } + + $dumpSwitches = @( + ,'--hangdump' + ,'--hangdump-timeout','120s' + ,'--crashdump' + ) + $mtpArgs = @( + ,'--coverage' + ,'--coverage-output-format','cobertura' + ,'--diagnostic' + ,'--diagnostic-output-directory',$testLogs + ,'--diagnostic-verbosity','Information' + ,'--results-directory',$testLogs + ,'--report-trx' + ) + + & $dotnet test --solution $RepoRoot ` + --no-build ` + -c $Configuration ` + -bl:"$testBinLog" ` + --filter-not-trait 'TestCategory=FailsInCloudTest' ` + --coverage-settings "$PSScriptRoot/test.runsettings" ` + @mtpArgs ` + @dumpSwitches ` + @extraArgs + if ($LASTEXITCODE -ne 0) { $failedTests += 1 } + + $trxFiles = Get-ChildItem -Recurse -Path $testLogs\*.trx +} else { + $testDiagLog = Join-Path $ArtifactStagingFolder (Join-Path test_logs diag.log) + & $dotnet test $RepoRoot ` + --no-build ` + -c $Configuration ` + --filter "TestCategory!=FailsInCloudTest" ` + --collect "Code Coverage;Format=cobertura" ` + --settings "$PSScriptRoot/test.runsettings" ` + --blame-hang-timeout 60s ` + --blame-crash ` + -bl:"$testBinLog" ` + --diag "$testDiagLog;TraceLevel=info" ` + --logger trx ` + @extraArgs + if ($LASTEXITCODE -ne 0) { $failedTests += 1 } + + $trxFiles = Get-ChildItem -Recurse -Path $RepoRoot\test\*.trx +} + +$unknownCounter = 0 +$trxFiles |% { + New-Item $testLogs -ItemType Directory -Force | Out-Null + if (!($_.FullName.StartsWith($testLogs, [StringComparison]::OrdinalIgnoreCase))) { + Copy-Item $_ -Destination $testLogs + } + + if ($PublishResults) { + $x = [xml](Get-Content -LiteralPath $_) + $runTitle = $null + if ($x.TestRun.TestDefinitions -and $x.TestRun.TestDefinitions.GetElementsByTagName('UnitTest')) { + $storage = $x.TestRun.TestDefinitions.GetElementsByTagName('UnitTest')[0].storage -replace '\\','/' + if ($storage -match '/(?net[^/]+)/(?:(?[^/]+)/)?(?[^/]+)\.(dll|exe)$') { + if ($matches.rid) { + $runTitle = "$($matches.lib) ($($matches.tfm), $($matches.rid), $Agent)" + } else { + $runTitle = "$($matches.lib) ($($matches.tfm)$x86RunTitleSuffix, $Agent)" + } + } + } + if (!$runTitle) { + $unknownCounter += 1; + $runTitle = "unknown$unknownCounter ($Agent$x86RunTitleSuffix)"; + } + + Write-Host "##vso[results.publish type=VSTest;runTitle=$runTitle;publishRunAttachments=true;resultFiles=$_;failTaskOnFailedTests=true;testRunSystem=VSTS - PTR;]" + } +} + +if ($failedTests -ne 0) { + exit $failedTests +} diff --git a/tools/publish-CodeCov.ps1 b/tools/publish-CodeCov.ps1 new file mode 100644 index 000000000..1d7365110 --- /dev/null +++ b/tools/publish-CodeCov.ps1 @@ -0,0 +1,30 @@ +<# +.SYNOPSIS + Uploads code coverage to codecov.io +.PARAMETER CodeCovToken + Code coverage token to use +.PARAMETER PathToCodeCoverage + Path to root of code coverage files +.PARAMETER Name + Name to upload with codecoverge +.PARAMETER Flags + Flags to upload with codecoverge +#> +[CmdletBinding()] +Param ( + [Parameter(Mandatory=$true)] + [string]$CodeCovToken, + [Parameter(Mandatory=$true)] + [string]$PathToCodeCoverage, + [string]$Name, + [string]$Flags +) + +$RepoRoot = (Resolve-Path "$PSScriptRoot/..").Path + +Get-ChildItem -Recurse -LiteralPath $PathToCodeCoverage -Filter "*.cobertura.xml" | % { + $relativeFilePath = Resolve-Path -relative $_.FullName + + Write-Host "Uploading: $relativeFilePath" -ForegroundColor Yellow + & (& "$PSScriptRoot/Get-CodeCovTool.ps1") -t $CodeCovToken -f $relativeFilePath -R $RepoRoot -F $Flags -n $Name +} diff --git a/tools/test.runsettings b/tools/test.runsettings new file mode 100644 index 000000000..4e24a0a65 --- /dev/null +++ b/tools/test.runsettings @@ -0,0 +1,44 @@ + + + + + + + + + \.dll$ + \.exe$ + + + xunit\..* + + + + + ^System\.Diagnostics\.DebuggerHiddenAttribute$ + ^System\.Diagnostics\.DebuggerNonUserCodeAttribute$ + ^System\.CodeDom\.Compiler\.GeneratedCodeAttribute$ + ^System\.Diagnostics\.CodeAnalysis\.ExcludeFromCodeCoverageAttribute$ + + + + + True + + True + + True + + False + + False + + False + + True + + + + + + diff --git a/tools/variables/BusinessGroupName.ps1 b/tools/variables/BusinessGroupName.ps1 new file mode 100644 index 000000000..008242662 --- /dev/null +++ b/tools/variables/BusinessGroupName.ps1 @@ -0,0 +1 @@ +'Visual Studio - VS Core' diff --git a/tools/variables/DotNetSdkVersion.ps1 b/tools/variables/DotNetSdkVersion.ps1 new file mode 100644 index 000000000..722cc5845 --- /dev/null +++ b/tools/variables/DotNetSdkVersion.ps1 @@ -0,0 +1,2 @@ +$globalJson = Get-Content -LiteralPath "$PSScriptRoot\..\..\global.json" | ConvertFrom-Json +$globalJson.sdk.version diff --git a/tools/variables/InsertJsonValues.ps1 b/tools/variables/InsertJsonValues.ps1 new file mode 100644 index 000000000..fb9390c53 --- /dev/null +++ b/tools/variables/InsertJsonValues.ps1 @@ -0,0 +1,32 @@ +$vstsDropNames = & "$PSScriptRoot\VstsDropNames.ps1" +$BuildConfiguration = $env:BUILDCONFIGURATION +if (!$BuildConfiguration) { + $BuildConfiguration = 'Debug' +} + +$BasePath = "$PSScriptRoot\..\..\bin\Packages\$BuildConfiguration\Vsix" + +if (Test-Path $BasePath) { + $vsmanFiles = @() + Get-ChildItem $BasePath *.vsman -Recurse -File | % { + $version = (Get-Content $_.FullName | ConvertFrom-Json).info.buildVersion + $fullPath = (Resolve-Path $_.FullName).Path + $basePath = (Resolve-Path $BasePath).Path + # Cannot use RelativePath or GetRelativePath due to Powershell Core v2.0 limitation + if ($fullPath.StartsWith($basePath, [StringComparison]::OrdinalIgnoreCase)) { + # Get the relative paths then make sure the directory separators match URL format. + $rfn = $fullPath.Substring($basePath.Length).TrimStart('\', '/').Replace('\', '/') + } + else { + $rfn = $fullPath # fallback to full path if it doesn't start with base path + } + + $fn = $_.Name + + # The left side is filename followed by the version and the right side is the drop url and the relative filename + $thisVsManFile = "$fn{$version}=https://vsdrop.corp.microsoft.com/file/v1/$vstsDropNames;$rfn" + $vsmanFiles += $thisVsManFile + } + + [string]::join(',', $vsmanFiles) +} diff --git a/azure-pipelines/variables/InsertPropsValues.ps1 b/tools/variables/InsertPropsValues.ps1 similarity index 54% rename from azure-pipelines/variables/InsertPropsValues.ps1 rename to tools/variables/InsertPropsValues.ps1 index 427198cad..3ae11de94 100644 --- a/azure-pipelines/variables/InsertPropsValues.ps1 +++ b/tools/variables/InsertPropsValues.ps1 @@ -1,10 +1,8 @@ -$BinPath = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..\bin\Packages\$env:BUILDCONFIGURATION") +$InsertedPkgs = (& "$PSScriptRoot\..\artifacts\VSInsertion.ps1") -$dirsToSearch = "$BinPath\NuGet\*.nupkg" |? { Test-Path $_ } $icv=@() - -if ($dirsToSearch) { - Get-ChildItem -Path $dirsToSearch |% { +foreach ($kvp in $InsertedPkgs.GetEnumerator()) { + $kvp.Value |% { if ($_.Name -match "^(.*?)\.(\d+\.\d+\.\d+(?:\.\d+)?(?:-.*?)?)(?:\.symbols)?\.nupkg$") { $id = $Matches[1] $version = $Matches[2] diff --git a/azure-pipelines/variables/InsertTargetBranch.ps1 b/tools/variables/InsertTargetBranch.ps1 similarity index 100% rename from azure-pipelines/variables/InsertTargetBranch.ps1 rename to tools/variables/InsertTargetBranch.ps1 diff --git a/tools/variables/InsertVersionsValues.ps1 b/tools/variables/InsertVersionsValues.ps1 new file mode 100644 index 000000000..05b4c9feb --- /dev/null +++ b/tools/variables/InsertVersionsValues.ps1 @@ -0,0 +1,5 @@ +$MacroName = 'MicrosoftVisualStudioThreadingVersion' +$SampleProject = "$PSScriptRoot\..\..\src\Microsoft.VisualStudio.Threading" +[string]::join(',',(@{ + ($MacroName) = & { (dotnet nbgv get-version --project $SampleProject --format json | ConvertFrom-Json).AssemblyVersion }; +}.GetEnumerator() |% { "$($_.key)=$($_.value)" })) diff --git a/azure-pipelines/variables/LocLanguages.ps1 b/tools/variables/LocLanguages.ps1 similarity index 100% rename from azure-pipelines/variables/LocLanguages.ps1 rename to tools/variables/LocLanguages.ps1 diff --git a/azure-pipelines/variables/ProfilingInputsDropName.ps1 b/tools/variables/ProfilingInputsDropName.ps1 similarity index 100% rename from azure-pipelines/variables/ProfilingInputsDropName.ps1 rename to tools/variables/ProfilingInputsDropName.ps1 diff --git a/tools/variables/ProfilingInputsPropsName.ps1 b/tools/variables/ProfilingInputsPropsName.ps1 new file mode 100644 index 000000000..9e6bc4a16 --- /dev/null +++ b/tools/variables/ProfilingInputsPropsName.ps1 @@ -0,0 +1,6 @@ +if ($env:SYSTEM_TEAMPROJECT) { + $repoName = $env:BUILD_REPOSITORY_NAME.Replace('/', '.') + "$env:SYSTEM_TEAMPROJECT.$repoName.props" +} else { + Write-Warning "No Azure Pipelines build detected. No profiling inputs filename will be computed." +} diff --git a/azure-pipelines/variables/ShouldSkipOptimize.ps1 b/tools/variables/ShouldSkipOptimize.ps1 similarity index 100% rename from azure-pipelines/variables/ShouldSkipOptimize.ps1 rename to tools/variables/ShouldSkipOptimize.ps1 diff --git a/azure-pipelines/variables/SymbolsFeatureName.ps1 b/tools/variables/SymbolsFeatureName.ps1 similarity index 100% rename from azure-pipelines/variables/SymbolsFeatureName.ps1 rename to tools/variables/SymbolsFeatureName.ps1 diff --git a/tools/variables/VstsDropNames.ps1 b/tools/variables/VstsDropNames.ps1 new file mode 100644 index 000000000..4ff36b2c9 --- /dev/null +++ b/tools/variables/VstsDropNames.ps1 @@ -0,0 +1 @@ +"Products/$env:SYSTEM_TEAMPROJECT/$env:BUILD_REPOSITORY_NAME/$env:BUILD_SOURCEBRANCHNAME/$env:BUILD_BUILDID" diff --git a/tools/variables/_all.ps1 b/tools/variables/_all.ps1 new file mode 100644 index 000000000..cc6e88105 --- /dev/null +++ b/tools/variables/_all.ps1 @@ -0,0 +1,20 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + This script returns a hashtable of build variables that should be set + at the start of a build or release definition's execution. +#> + +[CmdletBinding(SupportsShouldProcess = $true)] +param ( +) + +$vars = @{} + +Get-ChildItem "$PSScriptRoot\*.ps1" -Exclude "_*" |% { + Write-Host "Computing $($_.BaseName) variable" + $vars[$_.BaseName] = & $_ +} + +$vars diff --git a/azure-pipelines/variables/_pipelines.ps1 b/tools/variables/_define.ps1 similarity index 57% rename from azure-pipelines/variables/_pipelines.ps1 rename to tools/variables/_define.ps1 index 867b7fc8b..ba081f0fd 100644 --- a/azure-pipelines/variables/_pipelines.ps1 +++ b/tools/variables/_define.ps1 @@ -1,13 +1,24 @@ -# This script translates the variables returned by the _all.ps1 script -# into commands that instruct Azure Pipelines to actually set those variables for other pipeline tasks to consume. +<# +.SYNOPSIS + This script translates the variables returned by the _all.ps1 script + into commands that instruct Azure Pipelines or GitHub Actions to actually set those variables for other pipeline tasks to consume. -# The build or release definition may have set these variables to override -# what the build would do. So only set them if they have not already been set. + The build or release definition may have set these variables to override + what the build would do. So only set them if they have not already been set. +#> + +[CmdletBinding()] +param ( +) + +if ($env:GITHUB_ACTIONS) { + . "$PSScriptRoot\..\GitHubActions.ps1" +} (& "$PSScriptRoot\_all.ps1").GetEnumerator() |% { # Always use ALL CAPS for env var names since Azure Pipelines converts variable names to all caps and on non-Windows OS, env vars are case sensitive. $keyCaps = $_.Key.ToUpper() - if (Test-Path -Path "env:$keyCaps") { + if ((Test-Path "env:$keyCaps") -and (Get-Content "env:$keyCaps")) { Write-Host "Skipping setting $keyCaps because variable is already set to '$(Get-Content env:$keyCaps)'." -ForegroundColor Cyan } else { Write-Host "$keyCaps=$($_.Value)" -ForegroundColor Yellow @@ -17,8 +28,8 @@ # and the second that works across jobs and stages but must be fully qualified when referenced. Write-Host "##vso[task.setvariable variable=$keyCaps;isOutput=true]$($_.Value)" } elseif ($env:GITHUB_ACTIONS) { - Add-Content -Path $env:GITHUB_ENV -Value "$keyCaps=$($_.Value)" + Add-GitHubActionsEnvVariable -Name $keyCaps -Value ([string]$_.Value) } - Set-Item -Path "env:$keyCaps" -Value $_.Value + Set-Item -LiteralPath "env:$keyCaps" -Value $_.Value } } diff --git a/version.json b/version.json index e94b618dc..e53c28ed4 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { - "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "17.2-alpha", + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", + "version": "18.7", "publicReleaseRefSpec": [ "^refs/heads/main$", "^refs/heads/v\\d+(?:\\.\\d+)?$"